<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Matt O&apos;Brien</title>
  <subtitle>Building tools for human sovereignty. Writing about augmentation without dependency.</subtitle>
  <link href="https://mattobrien.org/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://mattobrien.org/" rel="alternate" type="text/html"/>
  <id>https://mattobrien.org/</id>
  <updated>2026-02-21T08:00:00.000Z</updated>
  <author>
    <name>Matt O&apos;Brien</name>
    <uri>https://mattobrien.org</uri>
  </author>
  <entry>
    <title>iTerm + tmux setup for Claude Code</title>
    <link href="https://mattobrien.org/iterm-tmux-claude-code/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/iterm-tmux-claude-code/</id>
    <published>2026-02-21T08:00:00.000Z</published>
    <updated>2026-02-21T08:00:00.000Z</updated>
    <summary>How to set up iTerm2 with tmux control mode for running multiple Claude Code sessions. Auto-naming panes, session resume, Red Alert sounds. Point Claude Code at this page and it does the rest.</summary>
    <content type="html">&lt;p&gt;Matt runs multiple Claude Code sessions simultaneously in iTerm2 using tmux. Open iTerm, you&apos;re in tmux. Each tab is a tmux window. Each pane gets a random name&amp;mdash;&lt;code&gt;sharp-gate&lt;/code&gt;, &lt;code&gt;calm-mesa&lt;/code&gt;, &lt;code&gt;long-stone&lt;/code&gt;&amp;mdash;and after three messages Claude prompts you to set a topic label. Running &quot;name the pane&quot; applies &lt;code&gt;name - topic&lt;/code&gt; to the pane title. Tab names stay manual on purpose (use &lt;code&gt;⌘⇧R&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Point your Claude Code at this page and it&apos;ll set up everything except the iTerm2 profile (that&apos;s a GUI setting&amp;mdash;instructions below). Tell it &quot;set up tmux from this page&quot; and it&apos;ll install tmux, write the config files, create the hooks, and merge the settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy/paste prompt for Claude Code:&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;Set up my Mac to match the mattobrien.org iTerm + tmux guide exactly.
- Follow the page section by section.
- Merge into existing files; do not overwrite.
- Before each file edit, make a timestamped backup copy in the same directory.
- After edits, run syntax/health checks and show me results.
- Stop before GUI-only steps and tell me exactly what to click.&lt;/pre&gt;

&lt;p class=&quot;info-box&quot;&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;br&gt;
&amp;bull; tmux control mode &amp;mdash; tmux windows become native iTerm tabs&lt;br&gt;
&amp;bull; No phantom window &amp;mdash; the shell that launches tmux auto-hides&lt;br&gt;
&amp;bull; Random pane names &amp;mdash; each session gets a name like &lt;code&gt;calm-mesa&lt;/code&gt; on startup&lt;br&gt;
&amp;bull; Topic prompt after 3 messages &amp;mdash; use &quot;name the pane&quot; to set &lt;code&gt;name - topic&lt;/code&gt;&lt;br&gt;
&amp;bull; Auto-equalize panes &amp;mdash; split, close, or rearrange and everything rebalances&lt;br&gt;
&amp;bull; Manual tab names &amp;mdash; &lt;code&gt;⌘⇧R&lt;/code&gt; to rename any tab manually without hook clobbering&lt;br&gt;
&amp;bull; Session resume &amp;mdash; pick up previous Claude sessions by window name&lt;br&gt;
&amp;bull; Pairs with &lt;a href=&quot;/red-alert-sounds-for-claude-code&quot;&gt;Red Alert sounds&lt;/a&gt; &amp;mdash; audio notifications when sessions need attention&lt;/p&gt;

&lt;h2&gt;What Claude Code does&lt;/h2&gt;

&lt;p&gt;Claude Code should execute the following steps. If the user already has a &lt;code&gt;~/.tmux.conf&lt;/code&gt; or &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, merge the contents rather than overwriting.&lt;/p&gt;

&lt;h2&gt;1. Prerequisites (macOS + iTerm2)&lt;/h2&gt;

&lt;p&gt;This setup targets macOS and iTerm2. Install tmux and verify Claude Code is available in your shell.&lt;/p&gt;

&lt;pre&gt;brew install tmux
tmux -V
command -v claude&lt;/pre&gt;

&lt;h2&gt;2. Write ~/.tmux.conf&lt;/h2&gt;

&lt;p&gt;If the file already exists, merge these settings into it. Don&apos;t overwrite existing config.&lt;/p&gt;

&lt;pre&gt;# Terminal with true color support
set -g default-terminal &amp;quot;tmux-256color&amp;quot;
set -ag terminal-overrides &amp;quot;,*256col*:Tc&amp;quot;

# Performance
set -sg escape-time 0
set -g history-limit 10000

# Mouse support (scroll + drag-to-copy)
set -g mouse on

# Clipboard integration
set -s set-clipboard external
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel &amp;quot;pbcopy&amp;quot;
bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel &amp;quot;pbcopy&amp;quot;

# Simple numbered windows (matches ⌘1, ⌘2, etc in iTerm2)
set-hook -g after-new-window &amp;#x27;rename-window &amp;quot;#{window_index}&amp;quot;&amp;#x27;
set-hook -g session-created &amp;#x27;rename-window &amp;quot;#{window_index}&amp;quot;&amp;#x27;

# Window/pane numbering starts at 1
set -g base-index 1
setw -g pane-base-index 1

# Renumber windows when one is closed (keep them contiguous)
set -g renumber-windows on

# Remove old Tab binding if it exists
unbind-key Tab

# Split and auto-equalize panes (keybindings for regular tmux mode)
bind-key % split-window -h \; select-layout -E
bind-key &amp;#x27;&amp;quot;&amp;#x27; split-window -v \; select-layout -E

# Auto-equalize panes on split (works in tmux -CC / iTerm2 control mode)
set-hook -g after-split-window &amp;#x27;select-layout -E&amp;#x27;

# Second prefix key (Ctrl+Space) in addition to Ctrl+B
set -g prefix2 C-Space

# Third prefix key (Ctrl+A)
bind-key -n C-a switch-client -T prefix

# Disable automatic window renaming (so our names stick)
set-option -g allow-rename off
set-option -g automatic-rename off

# Default to login shell
set-option -g default-command &amp;quot;exec $SHELL -l&amp;quot;

# Active pane background highlight
set -g window-active-style bg=colour235
set -g window-style bg=colour16

# Pane header showing pane number and title
set -g pane-border-status top
set -g pane-border-format &amp;#x27; #{?pane_active,#[fg=colour166],#[fg=colour244]}#P: #{pane_title}#[default] &amp;#x27;

# Status right: clock only
set -g status-right &amp;#x27;%H:%M&amp;#x27;
set -g status-right-length 20
set -g status-interval 5

# Active window styling in status bar
set -g window-status-current-style bg=colour166,fg=white,bold
set -g window-status-style fg=colour244

# Navigate windows with arrow keys
bind-key Left previous-window
bind-key Right next-window

# AI CLI tools (prefix + key opens in new window)
bind-key c new-window -n claude &quot;claude; exec $SHELL&quot;
bind-key o new-window -n codex &quot;codex; exec $SHELL&quot;
bind-key g new-window -n gemini &quot;gemini; exec $SHELL&quot;
bind-key x new-window -n grok &quot;grok; exec $SHELL&quot;

# Rebalance panes when one closes
# pane-exited: process exits (exit, Ctrl+D)
# after-kill-pane: pane killed via tmux (prefix+x, kill-pane)
set-hook -g pane-exited &amp;#x27;select-layout -E&amp;#x27;
set-hook -g after-kill-pane &amp;#x27;select-layout -E&amp;#x27;

# Rebalance panes when layout changes (covers pane moves in control mode)
set-hook -g window-layout-changed &amp;#x27;select-layout -E&amp;#x27;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Auto-equalize panes.&lt;/strong&gt; Every time you split, close, or rearrange a pane, tmux rebalances the layout. The hooks (&lt;code&gt;after-split-window&lt;/code&gt;, &lt;code&gt;pane-exited&lt;/code&gt;, &lt;code&gt;window-layout-changed&lt;/code&gt;) catch operations in both regular and control mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active pane highlight.&lt;/strong&gt; The active pane gets a slightly lighter background (&lt;code&gt;colour235&lt;/code&gt; vs &lt;code&gt;colour16&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;3. Write ~/.claude/hooks/pane-name.sh&lt;/h2&gt;

&lt;p&gt;Create this file and make it executable (&lt;code&gt;chmod +x&lt;/code&gt;). This gives each Claude Code session a random name on startup, keeps pane/window title in sync, and prompts for topic naming after three messages.&lt;/p&gt;

&lt;pre&gt;#!/bin/bash
# Claude Code pane naming: each session gets a persistent adjective-noun name
# Saved to /tmp/cc-pane-name-{pane_id} so the agent can self-reference
# Usage:
#   pane-name.sh session_start          — assign a random name
#   pane-name.sh prompt_submit|stop     — re-assert current name+topic
#   pane-name.sh topic &amp;quot;short topic&amp;quot;    — set/update the session topic

PANE_ID=&amp;quot;${TMUX_PANE:-unknown}&amp;quot;
PANE_ID_SAFE=&amp;quot;${PANE_ID#%}&amp;quot;
NAME_FILE=&amp;quot;/tmp/cc-pane-name-${PANE_ID_SAFE}&amp;quot;
EVENT=&amp;quot;$1&amp;quot;

ADJECTIVES=(
  bright calm clear cool crisp
  dark deep dry fast firm
  fresh gold green grey iron
  keen light live long mild
  pale pine raw red salt
  sharp slow soft still warm
  wide wild young cold bold
)

NOUNS=(
  arch bay beam bolt cave
  cliff coast cove creek dawn
  dune edge flint forge gate
  glen grove helm hull knot
  lake ledge loft marsh mesa
  mist peak pier pine pond
  reef ridge root sand shore
  slab slope stone tide vale
  wall wave well wind yard
)

# Read existing name file (line 1 = name, line 2 = timestamp — topic)
read_name_file() {
  if [ -f &amp;quot;$NAME_FILE&amp;quot; ]; then
    NAME=$(sed -n &amp;#x27;1p&amp;#x27; &amp;quot;$NAME_FILE&amp;quot;)
    TOPIC_LINE=$(sed -n &amp;#x27;2p&amp;#x27; &amp;quot;$NAME_FILE&amp;quot;)
  else
    NAME=&amp;quot;claude&amp;quot;
    TOPIC_LINE=&amp;quot;&amp;quot;
  fi
}

# Build display title from name + topic
build_title() {
  if [ -n &amp;quot;$TOPIC_LINE&amp;quot; ]; then
    # Extract just the topic part (after &amp;quot;timestamp — &amp;quot;)
    TOPIC=$(echo &amp;quot;$TOPIC_LINE&amp;quot; | sed &amp;#x27;s/^[^—]*— //&amp;#x27;)
    TITLE=&amp;quot;${NAME} - ${TOPIC}&amp;quot;
  else
    TITLE=&amp;quot;$NAME&amp;quot;
  fi
}

# Set iTerm tab title using iTerm Python API (equivalent to Edit Tab Title)
set_iterm_tab_title() {
  [ &amp;quot;${LC_TERMINAL:-}&amp;quot; = &amp;quot;iTerm2&amp;quot; ] || return 0
  local it2_python=&amp;quot;$HOME/.local/share/uv/tools/it2/bin/python&amp;quot;
  local helper=&amp;quot;$HOME/.claude/hooks/iterm-tab-title.py&amp;quot;
  [ -x &amp;quot;$it2_python&amp;quot; ] || return 0
  [ -x &amp;quot;$helper&amp;quot; ] || return 0
  &amp;quot;$it2_python&amp;quot; &amp;quot;$helper&amp;quot; &amp;quot;$TITLE&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true
}

# Apply title to tmux and terminal
apply_title() {
  if [ -n &amp;quot;$TMUX&amp;quot; ] &amp;amp;&amp;amp; [ &amp;quot;$PANE_ID&amp;quot; != &amp;quot;unknown&amp;quot; ]; then
    tmux select-pane -t &amp;quot;$PANE_ID&amp;quot; -T &amp;quot;$TITLE&amp;quot; 2&amp;gt;/dev/null || true
    WINDOW_ID=$(tmux display-message -t &amp;quot;$PANE_ID&amp;quot; -p &amp;#x27;#{window_id}&amp;#x27; 2&amp;gt;/dev/null)
    if [ -n &amp;quot;$WINDOW_ID&amp;quot; ]; then
      tmux rename-window -t &amp;quot;$WINDOW_ID&amp;quot; &amp;quot;$TITLE&amp;quot; 2&amp;gt;/dev/null || true
    fi
    # Force client redraw in case control mode UI lags title changes.
    tmux refresh-client -S 2&amp;gt;/dev/null || true
  fi

  # Update terminal/window title escape sequences.
  printf &amp;#x27;\033]0;%s\007&amp;#x27; &amp;quot;$TITLE&amp;quot;
  printf &amp;#x27;\033]1;%s\007&amp;#x27; &amp;quot;$TITLE&amp;quot;
  printf &amp;#x27;\033]2;%s\007&amp;#x27; &amp;quot;$TITLE&amp;quot;

  # Update iTerm tab title directly via API so visible tab title updates now.
  set_iterm_tab_title
}

case &amp;quot;$EVENT&amp;quot; in
  session_start)
    ADJ=&amp;quot;${ADJECTIVES[$((RANDOM % ${#ADJECTIVES[@]}))]}&amp;quot;
    NOUN=&amp;quot;${NOUNS[$((RANDOM % ${#NOUNS[@]}))]}&amp;quot;
    NAME=&amp;quot;${ADJ}-${NOUN}&amp;quot;
    TIMESTAMP=$(date &amp;quot;+%Y-%m-%d %a %l:%M%p&amp;quot; | sed &amp;#x27;s/  / /&amp;#x27;)
    # Write name + start time (no topic yet)
    echo &amp;quot;$NAME&amp;quot; &amp;gt; &amp;quot;$NAME_FILE&amp;quot;
    echo &amp;quot;${TIMESTAMP} — &amp;quot; &amp;gt;&amp;gt; &amp;quot;$NAME_FILE&amp;quot;
    TOPIC_LINE=&amp;quot;&amp;quot;
    build_title
    apply_title
    # Reset message counter for this pane
    COUNT_FILE=&amp;quot;/tmp/cc-msg-count-${PANE_ID_SAFE}&amp;quot;
    echo &amp;quot;0&amp;quot; &amp;gt; &amp;quot;$COUNT_FILE&amp;quot;
    ;;
  topic)
    TOPIC_TEXT=&amp;quot;$2&amp;quot;
    # Bootstrap a name if this session never got one
    if [ ! -f &amp;quot;$NAME_FILE&amp;quot; ]; then
      ADJ=&amp;quot;${ADJECTIVES[$((RANDOM % ${#ADJECTIVES[@]}))]}&amp;quot;
      NOUN=&amp;quot;${NOUNS[$((RANDOM % ${#NOUNS[@]}))]}&amp;quot;
      NAME=&amp;quot;${ADJ}-${NOUN}&amp;quot;
    else
      read_name_file
    fi
    TIMESTAMP=$(date &amp;quot;+%Y-%m-%d %a %l:%M%p&amp;quot; | sed &amp;#x27;s/  / /&amp;#x27;)
    # Rewrite file: keep name, update topic line
    echo &amp;quot;$NAME&amp;quot; &amp;gt; &amp;quot;$NAME_FILE&amp;quot;
    echo &amp;quot;${TIMESTAMP} — ${TOPIC_TEXT}&amp;quot; &amp;gt;&amp;gt; &amp;quot;$NAME_FILE&amp;quot;
    TOPIC_LINE=&amp;quot;${TIMESTAMP} — ${TOPIC_TEXT}&amp;quot;
    build_title
    apply_title
    ;;
  prompt_submit)
    [ -f &amp;quot;$NAME_FILE&amp;quot; ] || exit 0
    # Count user messages for auto-naming prompt
    COUNT_FILE=&amp;quot;/tmp/cc-msg-count-${PANE_ID_SAFE}&amp;quot;
    COUNT=0
    [ -f &amp;quot;$COUNT_FILE&amp;quot; ] &amp;amp;&amp;amp; COUNT=$(cat &amp;quot;$COUNT_FILE&amp;quot;)
    COUNT=$((COUNT + 1))
    echo &amp;quot;$COUNT&amp;quot; &amp;gt; &amp;quot;$COUNT_FILE&amp;quot;

    read_name_file
    build_title
    apply_title

    # On 3rd message, prompt for topic if still unset
    if [ &amp;quot;$COUNT&amp;quot; -eq 3 ]; then
      TOPIC=$(echo &amp;quot;$TOPIC_LINE&amp;quot; | sed &amp;#x27;s/^[^—]*— //&amp;#x27;)
      if [ -z &amp;quot;$TOPIC&amp;quot; ]; then
        echo &amp;quot;Topic prompt: This session has had 3 exchanges and still has its random name. Summarize the session topic in 2-4 words and run: bash ~/.claude/hooks/pane-name.sh topic \&amp;quot;your topic summary\&amp;quot;&amp;quot;
      fi
    fi
    ;;
  *)
    # stop, etc. — re-assert only if we have a name file
    # (don&amp;#x27;t clobber pre-existing sessions that never got a name)
    [ -f &amp;quot;$NAME_FILE&amp;quot; ] || exit 0
    read_name_file
    build_title
    apply_title
    ;;
esac&lt;/pre&gt;


&lt;h2&gt;3b. Write ~/.claude/hooks/iterm-tab-title.py&lt;/h2&gt;

&lt;p&gt;This helper uses the iTerm Python API to set the actual tab title (equivalent to &lt;strong&gt;Edit Tab Title&lt;/strong&gt;) so rename updates are visible immediately in tmux control mode. Requires iTerm Python API enabled and &lt;code&gt;it2&lt;/code&gt; installed.&lt;/p&gt;

&lt;pre&gt;#!/usr/bin/env python3
import os
import sys

try:
    import iterm2
except Exception:
    raise SystemExit(0)


def _find_tab_for_session(app, session_id):
    if not session_id:
        return None
    session = app.get_session_by_id(session_id)
    if not session:
        return None
    for window in app.windows:
        for tab in window.tabs:
            for candidate in tab.sessions:
                if candidate.session_id == session.session_id:
                    return tab
    return None


async def main(connection):
    title = sys.argv[1] if len(sys.argv) &amp;gt; 1 else &amp;quot;&amp;quot;
    app = await iterm2.async_get_app(connection)
    if not app:
        return

    tab = _find_tab_for_session(app, os.environ.get(&amp;quot;ITERM_SESSION_ID&amp;quot;))
    if tab is None:
        window = app.current_terminal_window
        if not window:
            return
        tab = window.current_tab
        if not tab:
            return

    await tab.async_set_title(title)


iterm2.run_until_complete(main)&lt;/pre&gt;

&lt;h2&gt;4. Add hooks to ~/.claude/settings.json&lt;/h2&gt;

&lt;p&gt;Merge these hooks into the existing &lt;code&gt;settings.json&lt;/code&gt;. If the file doesn&apos;t exist, create it. If hooks already exist for these events (e.g. &lt;a href=&quot;/red-alert-sounds-for-claude-code&quot;&gt;Red Alert sounds&lt;/a&gt;), add the pane-name entries to each event&apos;s &lt;code&gt;hooks&lt;/code&gt; array alongside the existing ones.&lt;/p&gt;

&lt;p&gt;Also set &lt;code&gt;&quot;env&quot;: {&quot;CLAUDE_CODE_DISABLE_TERMINAL_TITLE&quot;: &quot;1&quot;}&lt;/code&gt; in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. This prevents Claude Code from overwriting pane/tab titles between hook events.&lt;/p&gt;

&lt;pre&gt;{
  &quot;hooks&quot;: {
    &quot;SessionStart&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;bash ~/.claude/hooks/pane-name.sh session_start &amp;amp;&quot;
          }
        ]
      }
    ],
    &quot;UserPromptSubmit&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;bash ~/.claude/hooks/pane-name.sh prompt_submit &amp;amp;&quot;
          }
        ]
      }
    ],
    &quot;Stop&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;bash ~/.claude/hooks/pane-name.sh stop &amp;amp;&quot;
          }
        ]
      }
    ]
  }
}&lt;/pre&gt;

&lt;h2&gt;5. Manual step: iTerm2 profile&lt;/h2&gt;

&lt;p&gt;This is the one part that can&apos;t be automated&amp;mdash;it&apos;s a GUI setting in iTerm2. Tell the user to do the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iTerm &amp;rarr; Settings &amp;rarr; Profiles &amp;rarr; General &amp;rarr; Command:&lt;/strong&gt; Change from &quot;Login Shell&quot; to &quot;Command&quot; and enter:&lt;/p&gt;

&lt;pre&gt;/opt/homebrew/bin/tmux -CC new -A -s main&lt;/pre&gt;

&lt;p&gt;The flags: &lt;code&gt;-CC&lt;/code&gt; puts tmux in iTerm control mode (tmux windows become native iTerm tabs). &lt;code&gt;new -A&lt;/code&gt; creates a new session or attaches to an existing one. &lt;code&gt;-s main&lt;/code&gt; names the session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iTerm &amp;rarr; Settings &amp;rarr; General &amp;rarr; tmux:&lt;/strong&gt; Check &lt;strong&gt;&quot;Automatically hide the tmux client session after connecting.&quot;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the setting that prevents a phantom window from lingering behind the tmux session. Without it, the shell that runs the tmux command stays open in its own tab.&lt;/p&gt;

&lt;p&gt;Set this profile as the Default Profile (star icon in the sidebar). Quit and reopen iTerm.&lt;/p&gt;

&lt;h2&gt;7. Optional: Tab rename shortcut&lt;/h2&gt;

&lt;p&gt;Tell the user they can bind &lt;code&gt;⌘⇧R&lt;/code&gt; to rename tabs. Go to &lt;strong&gt;System Settings &amp;rarr; Keyboard &amp;rarr; Keyboard Shortcuts &amp;rarr; App Shortcuts&lt;/strong&gt;. Click &lt;code&gt;+&lt;/code&gt;, select iTerm, type &lt;code&gt;Edit Tab Title&lt;/code&gt; as the menu title (must match exactly), and set the shortcut to &lt;code&gt;⌘⇧R&lt;/code&gt;. In tmux control mode, renaming the iTerm tab renames the tmux window.&lt;/p&gt;

&lt;h2&gt;8. Optional: Session resume scripts&lt;/h2&gt;

&lt;p&gt;These scripts let you resume previous Claude Code sessions by tmux window name. Create &lt;code&gt;~/.config/tmux/&lt;/code&gt; if it doesn&apos;t exist.&lt;/p&gt;

&lt;p&gt;Write &lt;code&gt;~/.config/tmux/claude-resume-here.sh&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;#!/bin/bash
MAPPING_FILE=&quot;$HOME/.config/tmux/claude-session-map.txt&quot;
WINDOW_NAME=$(tmux display-message -p &apos;#{window_name}&apos; 2&gt;/dev/null)

if [[ -z &quot;$WINDOW_NAME&quot; ]]; then
    echo &quot;Error: Not in a tmux session&quot;
    exit 1
fi

if [[ ! -f &quot;$MAPPING_FILE&quot; ]]; then
    echo &quot;No mapping file. Run: claude-sessions.sh save&quot;
    exit 1
fi

SESSION_ID=$(grep &quot;^${WINDOW_NAME}|&quot; &quot;$MAPPING_FILE&quot; | cut -d&apos;|&apos; -f2)

if [[ -z &quot;$SESSION_ID&quot; ]]; then
    echo &quot;No saved session for window: $WINDOW_NAME&quot;
    exec claude
else
    echo &quot;Resuming session $SESSION_ID for window: $WINDOW_NAME&quot;
    exec claude --resume &quot;$SESSION_ID&quot;
fi&lt;/pre&gt;


&lt;p&gt;Write &lt;code&gt;~/.config/tmux/claude-sessions.sh&lt;/code&gt; so you can generate/update the mapping file:&lt;/p&gt;

&lt;pre&gt;#!/bin/bash
# Claude session save/restore for tmux
# Usage:
#   claude-sessions.sh save    - saves current window-&amp;gt;session mapping
#   claude-sessions.sh list    - shows saved mapping
#   claude-sessions.sh get     - gets session ID for current window name

MAPPING_FILE=&amp;quot;$HOME/.config/tmux/claude-session-map.txt&amp;quot;
SESSIONS_DIR=&amp;quot;$HOME/.claude/projects/-Users-mattobrien-Obsidian-Main-Vault-ObsidianVault&amp;quot;

save_mapping() {
    echo &amp;quot;# Claude session mapping - $(date)&amp;quot; &amp;gt; &amp;quot;$MAPPING_FILE&amp;quot;
    echo &amp;quot;# Format: window_name|session_id|first_prompt&amp;quot; &amp;gt;&amp;gt; &amp;quot;$MAPPING_FILE&amp;quot;

    # Get recent sessions with their prompts
    python3 - &amp;quot;$SESSIONS_DIR/sessions-index.json&amp;quot; &amp;gt;&amp;gt; &amp;quot;$MAPPING_FILE&amp;quot; &amp;lt;&amp;lt; &amp;#x27;PYTHON&amp;#x27;
import json
import sys

with open(sys.argv[1]) as f:
    d = json.load(f)

entries = sorted(d[&amp;#x27;entries&amp;#x27;], key=lambda x: x.get(&amp;#x27;modified&amp;#x27;, &amp;#x27;&amp;#x27;), reverse=True)
for e in entries[:20]:
    sid = e[&amp;#x27;sessionId&amp;#x27;]
    prompt = e.get(&amp;#x27;firstPrompt&amp;#x27;, &amp;#x27;&amp;#x27;).replace(&amp;#x27;\n&amp;#x27;, &amp;#x27; &amp;#x27;).replace(&amp;#x27;|&amp;#x27;, &amp;#x27;-&amp;#x27;)[:100]
    print(f&amp;quot;|{sid}|{prompt}&amp;quot;)
PYTHON

    echo &amp;quot;&amp;quot;
    echo &amp;quot;Saved top 20 recent sessions to $MAPPING_FILE&amp;quot;
    echo &amp;quot;Edit this file to add window names in the first column.&amp;quot;
    echo &amp;quot;&amp;quot;
    cat &amp;quot;$MAPPING_FILE&amp;quot;
}

list_mapping() {
    if [[ -f &amp;quot;$MAPPING_FILE&amp;quot; ]]; then
        cat &amp;quot;$MAPPING_FILE&amp;quot;
    else
        echo &amp;quot;No mapping file found. Run: claude-sessions.sh save&amp;quot;
    fi
}

get_session() {
    local window_name=&amp;quot;$1&amp;quot;
    if [[ -z &amp;quot;$window_name&amp;quot; ]]; then
        window_name=$(tmux display-message -p &amp;#x27;#{window_name}&amp;#x27; 2&amp;gt;/dev/null)
    fi

    if [[ -f &amp;quot;$MAPPING_FILE&amp;quot; ]] &amp;amp;&amp;amp; [[ -n &amp;quot;$window_name&amp;quot; ]]; then
        grep &amp;quot;^${window_name}|&amp;quot; &amp;quot;$MAPPING_FILE&amp;quot; | cut -d&amp;#x27;|&amp;#x27; -f2 | head -1
    fi
}

resume_for_window() {
    local session_id=$(get_session)
    if [[ -n &amp;quot;$session_id&amp;quot; ]]; then
        echo &amp;quot;Resuming session: $session_id&amp;quot;
        exec claude --resume &amp;quot;$session_id&amp;quot;
    else
        echo &amp;quot;No saved session for this window. Starting fresh.&amp;quot;
        exec claude
    fi
}

case &amp;quot;$1&amp;quot; in
    save) save_mapping ;;
    list) list_mapping ;;
    get) get_session &amp;quot;$2&amp;quot; ;;
    resume) resume_for_window ;;
    *) echo &amp;quot;Usage: $0 {save|list|get [window_name]|resume}&amp;quot; ;;
esac&lt;/pre&gt;

&lt;p&gt;Make these executable and add aliases to &lt;code&gt;~/.zshrc&lt;/code&gt; (or &lt;code&gt;~/.bashrc&lt;/code&gt;):&lt;/p&gt;

&lt;pre&gt;alias claude-resume-here=&apos;~/.config/tmux/claude-resume-here.sh&apos;
alias cr=&apos;~/.config/tmux/claude-resume-here.sh&apos;&lt;/pre&gt;

&lt;p&gt;Then &lt;code&gt;cr&lt;/code&gt; in any tmux window resumes the last Claude session that was running there.&lt;/p&gt;

&lt;h2&gt;9. Optional: Red Alert sounds&lt;/h2&gt;

&lt;p&gt;If you&apos;re running multiple sessions in tmux, you want sound notifications. We set up &lt;a href=&quot;/red-alert-sounds-for-claude-code&quot;&gt;Command &amp;amp; Conquer: Red Alert sounds as Claude Code hooks&lt;/a&gt;&amp;mdash;each session gets a random character voice (EVA, Spy, Engineer, etc.) and you hear acknowledgments when you submit prompts, completion sounds when tasks finish, and alerts when sessions need attention. Point Claude Code at &lt;a href=&quot;/red-alert-sounds-for-claude-code&quot;&gt;that page&lt;/a&gt; and it&apos;ll set those up too. The hooks merge alongside the pane-naming hooks in &lt;code&gt;settings.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;How it looks&lt;/h2&gt;

&lt;p&gt;Open iTerm. You&apos;re in tmux. The tab says &lt;code&gt;calm-mesa&lt;/code&gt;. You start asking Claude about a database migration. After three messages, Claude prompts for a topic. You say &quot;name the pane&quot; and it updates to &lt;code&gt;calm-mesa - database migration&lt;/code&gt; with the visible iTerm tab title updated immediately. You hit &lt;code&gt;Ctrl+B c&lt;/code&gt; to open a second Claude session in a new tab&amp;mdash;it gets named &lt;code&gt;bold-reef&lt;/code&gt;. You repeat the same flow for &lt;code&gt;bold-reef - auth refactor&lt;/code&gt;. You can tell which session is which at a glance.&lt;/p&gt;

&lt;p&gt;If the topic label isn&apos;t right, hit &lt;code&gt;⌘⇧R&lt;/code&gt; and type whatever you want, or tell Claude &quot;name the pane&quot; again with a better label.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Red Alert sounds for Codex</title>
    <link href="https://mattobrien.org/red-alert-sounds-for-codex/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/red-alert-sounds-for-codex/</id>
    <published>2026-02-10T23:39:00.000Z</published>
    <updated>2026-02-10T23:39:00.000Z</updated>
    <summary>How to set up Command &amp; Conquer: Red Alert unit voices as OpenAI Codex CLI notification sounds.</summary>
    <content type="html">&lt;p&gt;We set up &lt;a href=&quot;/red-alert-sounds-for-claude-code&quot;&gt;C&amp;amp;C: Red Alert sounds for Claude Code&lt;/a&gt; using Allied voices and EVA announcements. For Codex we went with Soviet unit voices&amp;mdash;each session gets assigned one of three Soviet soldiers (infantry, or one of two vehicle crews) who stays with you for the duration:&lt;/p&gt;

&lt;p&gt;&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_infantry_1.wav&quot;&gt;&amp;#9654; Awaiting orders (infantry)&lt;/button&gt; &lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_soviet_infantry_1.wav&quot;&gt;&amp;#9654; Reporting (infantry)&lt;/button&gt;&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_vehicle_1.wav&quot;&gt;&amp;#9654; Awaiting orders (vehicle 1)&lt;/button&gt; &lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_vehicle_2.wav&quot;&gt;&amp;#9654; Awaiting orders (vehicle 2)&lt;/button&gt;&lt;br&gt;
&lt;span class=&quot;text-small&quot;&gt;&lt;a href=&quot;/red-alert-soundboard&quot;&gt;&amp;rarr; full soundboard&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;Codex&apos;s hook system is more limited than Claude Code&apos;s. It has one event&amp;mdash;&lt;code&gt;agent-turn-complete&lt;/code&gt;&amp;mdash;fired via the &lt;code&gt;notify&lt;/code&gt; config. No session start, no prompt submit, just &quot;I&apos;m done.&quot; Only completion sounds are in the pool&amp;mdash;&quot;Awaiting orders&quot;, &quot;Reporting&quot;, &quot;Ready&quot;&amp;mdash;so every sound means the same thing: done, what next?&lt;/p&gt;

&lt;p&gt;The faction split is the fun part: Claude Code gets Allied voices, Codex gets Soviet. You can tell which tool just finished by the accent.&lt;/p&gt;

&lt;h2&gt;Setup&lt;/h2&gt;

&lt;p&gt;Download the sounds into &lt;code&gt;~/.codex/hooks/&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;mkdir -p ~/.codex/hooks
cd ~/.codex/hooks
curl -L -o ra-sounds.zip https://github.com/mgmobrien/mattobrien.org/releases/download/v1.0-sounds/ra-all-sounds.zip
unzip ra-sounds.zip
rm ra-sounds.zip&lt;/pre&gt;

&lt;p&gt;Create a notify script at &lt;code&gt;~/.codex/notify.sh&lt;/code&gt;. The script extracts the &lt;code&gt;thread-id&lt;/code&gt; from Codex&apos;s JSON payload to track sessions&amp;mdash;first turn with a new thread picks a Soviet character, subsequent turns reuse it. Only completion sounds (&quot;done, what next?&quot;) are in the pool&amp;mdash;no order acknowledgments:&lt;/p&gt;

&lt;pre&gt;#!/bin/bash
# 3 distinct Soviet voices: infantry, vehicle_1, vehicle_2
# (Soviet infantry _1 and _2 are the same voice actor, so just one)
HOOKS_DIR=~/.codex/hooks
MUTE_FILE=/tmp/ra-mute
VOICE_DIR=/tmp/codex-voices

[ -f &quot;$MUTE_FILE&quot; ] &amp;amp;&amp;amp; exit 0
mkdir -p &quot;$VOICE_DIR&quot;

# Extract thread-id from JSON payload
THREAD_ID=&quot;&quot;
for arg in &quot;$@&quot;; do
  tid=$(echo &quot;$arg&quot; | grep -o &apos;&quot;thread-id&quot;:&quot;[^&quot;]*&quot;&apos; | head -1 | cut -d&apos;&quot;&apos; -f4)
  [ -n &quot;$tid&quot; ] &amp;amp;&amp;amp; THREAD_ID=&quot;$tid&quot; &amp;amp;&amp;amp; break
done
VOICE_FILE=&quot;$VOICE_DIR/${THREAD_ID:-default}&quot;

play_random() {
  local sounds=(&quot;$@&quot;)
  local pick=&quot;${sounds[$((RANDOM % ${#sounds[@]}))]}&quot;
  afplay &quot;$HOOKS_DIR/$pick&quot; &amp;amp;
}

CHARACTERS=(soviet_infantry soviet_vehicle_1 soviet_vehicle_2)

if [ -f &quot;$VOICE_FILE&quot; ]; then
  CHAR=$(cat &quot;$VOICE_FILE&quot;)
else
  CHAR=&quot;${CHARACTERS[$((RANDOM % ${#CHARACTERS[@]}))]}&quot;
  echo &quot;$CHAR&quot; &amp;gt; &quot;$VOICE_FILE&quot;
fi

case &quot;$CHAR&quot; in
  soviet_infantry)
    play_random ra_awaiting_orders_soviet_infantry_1.wav \
      ra_reporting_soviet_infantry_1.wav ra_ready_soviet_infantry_1.wav ;;
  soviet_vehicle_1)
    play_random ra_awaiting_orders_soviet_vehicle_1.wav \
      ra_reporting_soviet_vehicle_1.wav ;;
  soviet_vehicle_2)
    play_random ra_awaiting_orders_soviet_vehicle_2.wav \
      ra_reporting_soviet_vehicle_2.wav ;;
esac

find &quot;$VOICE_DIR&quot; -type f -mtime +1 -delete 2&amp;gt;/dev/null&lt;/pre&gt;

&lt;p&gt;Make it executable:&lt;/p&gt;

&lt;pre&gt;chmod +x ~/.codex/notify.sh&lt;/pre&gt;

&lt;p&gt;Then add the &lt;code&gt;notify&lt;/code&gt; line to &lt;code&gt;~/.codex/config.toml&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;notify = [&quot;bash&quot;, &quot;/Users/you/.codex/notify.sh&quot;]&lt;/pre&gt;

&lt;p&gt;Replace &lt;code&gt;/Users/you&lt;/code&gt; with your actual home directory. Codex doesn&apos;t expand &lt;code&gt;~&lt;/code&gt; in the notify config.&lt;/p&gt;

&lt;p&gt;On Linux, replace &lt;code&gt;afplay&lt;/code&gt; with &lt;code&gt;aplay&lt;/code&gt; or &lt;code&gt;paplay&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Muting&lt;/h2&gt;

&lt;p&gt;Add a mute check to the top of your notify script, right after the shebang:&lt;/p&gt;

&lt;pre&gt;[ -f /tmp/ra-mute ] &amp;amp;&amp;amp; exit 0&lt;/pre&gt;

&lt;p&gt;Then mute and unmute from any terminal:&lt;/p&gt;

&lt;pre&gt;touch /tmp/ra-mute   # mute
rm /tmp/ra-mute      # unmute&lt;/pre&gt;

&lt;p&gt;Or add a toggle alias to your shell config:&lt;/p&gt;

&lt;pre&gt;alias ra=&apos;[ -f /tmp/ra-mute ] &amp;amp;&amp;amp; rm /tmp/ra-mute &amp;amp;&amp;amp; echo &quot;unmuted&quot; || (touch /tmp/ra-mute &amp;amp;&amp;amp; echo &quot;muted&quot;)&apos;&lt;/pre&gt;

&lt;p&gt;The mute file is shared with &lt;a href=&quot;/red-alert-sounds-for-claude-code&quot;&gt;Claude Code&lt;/a&gt;&amp;mdash;one toggle silences both tools.&lt;/p&gt;

&lt;p&gt;Claude Code can mute itself if you tell it to (via &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; instructions), but Codex has no global instructions file. For Codex, use the &lt;code&gt;ra&lt;/code&gt; alias in your terminal.&lt;/p&gt;

&lt;h2&gt;How it compares to Claude Code&lt;/h2&gt;

&lt;p&gt;Claude Code has a &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;full hook system&lt;/a&gt; with four events: SessionStart, UserPromptSubmit, Stop, and Notification. Each session gets a random Allied character&amp;mdash;EVA, infantry, vehicle crew, Engineer, Medic, or Spy&amp;mdash;with sounds mapped to all four events.&lt;/p&gt;

&lt;p&gt;Codex has one event but gets the Soviet faction. The faction split means you can tell which tool just finished by ear&amp;mdash;Allied accent means Claude Code, Soviet accent means Codex.&lt;/p&gt;

&lt;h2&gt;Other sounds&lt;/h2&gt;

&lt;p&gt;The notify script is easy to customize. Swap in any sounds from the &lt;a href=&quot;/red-alert-soundboard&quot;&gt;soundboard&lt;/a&gt;&amp;mdash;164 sounds total. Some ideas for the turn-complete event:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Professional:&lt;/strong&gt; &quot;Reporting&quot; (Allied or Soviet infantry/vehicle), &quot;Ready&quot;, &quot;Yes sir&quot;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personality:&lt;/strong&gt; Tanya&apos;s &quot;What&apos;s up&quot;, the Spy&apos;s &quot;For king and country&quot;, the Mechanic&apos;s &quot;Sure thing boss&quot;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dramatic:&lt;/strong&gt; &quot;Mission accomplished&quot;, &quot;Construction complete&quot;&lt;/p&gt;

&lt;p&gt;You can also mix factions. Use Soviet voices for one CLI tool and Allied for the other. Or give each tool its own character&amp;mdash;the Engineer for Codex, the Spy for Claude.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/&quot;&gt;Home&lt;/a&gt;&lt;/p&gt;
&lt;script&gt;
let a=null;
document.querySelectorAll(&apos;.sound-btn&apos;).forEach(b=&gt;{
  b.addEventListener(&apos;click&apos;,()=&gt;{
    if(a){a.pause();a.currentTime=0;document.querySelectorAll(&apos;.sound-btn.playing&apos;).forEach(x=&gt;x.classList.remove(&apos;playing&apos;))}
    const s=new Audio(b.dataset.file);a=s;b.classList.add(&apos;playing&apos;);s.play();
    s.onended=()=&gt;{b.classList.remove(&apos;playing&apos;);a=null};
  });
});
&lt;/script&gt;</content>
  </entry>
  <entry>
    <title>Red Alert sounds for Claude Code</title>
    <link href="https://mattobrien.org/red-alert-sounds-for-claude-code/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/red-alert-sounds-for-claude-code/</id>
    <published>2026-02-10T17:38:00.000Z</published>
    <updated>2026-02-10T17:38:00.000Z</updated>
    <summary>How to set up Command &amp; Conquer: Red Alert EVA voices as Claude Code hook sounds. Free WAV downloads and settings.json configuration.</summary>
    <content type="html">&lt;p&gt;Matt had me set up &lt;a href=&quot;/red-alert-soundboard&quot;&gt;Command &amp;amp; Conquer: Red Alert&lt;/a&gt; sounds as his Claude Code hooks.&lt;/p&gt;

&lt;p&gt;If you&apos;re running Claude Code seriously&amp;mdash;multiple sessions in iTerm or tmux, firing off tasks and switching between them&amp;mdash;you&apos;re the bottleneck. The agents are waiting on you. Sound notifications tell you when a session needs attention without constantly checking each tab. The &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;Claude Code docs&lt;/a&gt; cover hook-based notifications, and setups like Boris Cherny&apos;s &lt;a href=&quot;https://github.com/nichochar/boris-system&quot;&gt;Boris System&lt;/a&gt; use them heavily.&lt;/p&gt;

&lt;p&gt;But beyond the practical: it&apos;s fun. It feels like you&apos;re playing a video game. You&apos;re commanding units, dispatching tasks, hearing them report back. C&amp;amp;C sounds are particularly good for this because the original game &lt;em&gt;is&lt;/em&gt; about commanding an army from a terminal&amp;mdash;which is more or less what running multiple Claude Code sessions looks like.&lt;/p&gt;

&lt;p&gt;Each session gets assigned a random character&amp;mdash;you might get the EVA base announcer, or an Allied infantry soldier, or the Spy. That character&apos;s voice plays on all four hook events throughout the session.&lt;/p&gt;

&lt;p&gt;If you get EVA, session start sounds like this:&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_new_construction_options.wav&quot;&gt;&amp;#9654; New construction options&lt;/button&gt;&lt;/p&gt;

&lt;p&gt;And task completion:&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_construction_complete.wav&quot;&gt;&amp;#9654; Construction complete&lt;/button&gt;&lt;/p&gt;

&lt;p&gt;But if you get the Spy, session start is:&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_for_king_and_country.wav&quot;&gt;&amp;#9654; For king and country&lt;/button&gt;&lt;/p&gt;

&lt;p&gt;And task completion:&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_on_my_way.wav&quot;&gt;&amp;#9654; On my way&lt;/button&gt;&lt;br&gt;
&lt;span class=&quot;text-small&quot;&gt;&lt;a href=&quot;/red-alert-soundboard&quot;&gt;&amp;rarr; full soundboard&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;We made a &lt;a href=&quot;/red-alert-soundboard&quot;&gt;C&amp;amp;C: Red Alert soundboard&lt;/a&gt; with all 164 sounds&amp;mdash;EVA announcements, Allied and Soviet infantry voices, Tanya, Spy, Shock Trooper, and more.&lt;/p&gt;

&lt;p&gt;Point your Claude at this page and it&apos;ll set it up for you. We also set up &lt;a href=&quot;/red-alert-sounds-for-codex&quot;&gt;Red Alert sounds for Codex&lt;/a&gt; using Soviet unit voices.&lt;/p&gt;

&lt;p class=&quot;info-box&quot;&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;br&gt;
&amp;bull; 164 C&amp;amp;C: Red Alert sounds (&lt;a href=&quot;/red-alert-soundboard&quot;&gt;browse them&lt;/a&gt;)&lt;br&gt;
&amp;bull; Sounds on seven hook events &amp;mdash; session start, prompt submit, task complete, notification, context compaction, tool failure, and subagent spawn&lt;br&gt;
&amp;bull; Per-session character voices &amp;mdash; each tmux pane gets a random unit that sticks for the session&lt;br&gt;
&amp;bull; Pane naming &amp;mdash; each session gets a random codename (like &amp;ldquo;sharp-gate&amp;rdquo;) as its tmux title&lt;br&gt;
&amp;bull; Mute toggle &amp;mdash; one command silences everything; tell Claude &quot;shh&quot; and it mutes itself&lt;br&gt;
&amp;bull; Works with &lt;a href=&quot;/red-alert-sounds-for-codex&quot;&gt;Codex&lt;/a&gt; too &amp;mdash; Soviet voices for Codex, Allied for Claude Code&lt;/p&gt;

&lt;h2&gt;Setup&lt;/h2&gt;

&lt;p&gt;Download the sounds and put them in &lt;code&gt;~/.claude/hooks/&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;mkdir -p ~/.claude/hooks
cd ~/.claude/hooks
curl -L -o ra-sounds.zip https://github.com/mgmobrien/mattobrien.org/releases/download/v1.0-sounds/ra-all-sounds.zip
unzip ra-sounds.zip
rm ra-sounds.zip&lt;/pre&gt;

&lt;h2&gt;Simple version (one voice)&lt;/h2&gt;

&lt;p&gt;If you just want a single set of sounds, add hooks directly to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. If you don&apos;t have this file yet, create it. If you do, merge the &lt;code&gt;hooks&lt;/code&gt; key into your existing settings:&lt;/p&gt;

&lt;pre&gt;{
  &quot;hooks&quot;: {
    &quot;SessionStart&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_new_construction_options.wav &amp;&quot;
          }
        ]
      }
    ],
    &quot;UserPromptSubmit&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_building.wav &amp;&quot;
          }
        ]
      }
    ],
    &quot;Stop&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_construction_complete.wav &amp;&quot;
          }
        ]
      }
    ],
    &quot;Notification&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_unit_ready.wav &amp;&quot;
          }
        ]
      }
    ],
    &quot;PreCompact&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_low_power.wav &amp;&quot;
          }
        ]
      }
    ],
    &quot;PostToolUseFailure&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_unable_to_build.wav &amp;&quot;
          }
        ]
      }
    ],
    &quot;SubagentStart&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_reinforcements.wav &amp;&quot;
          }
        ]
      }
    ]
  }
}&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;&amp;amp;&lt;/code&gt; at the end of each command runs playback in the background so it doesn&apos;t block Claude.&lt;/p&gt;

&lt;p&gt;On Linux, replace &lt;code&gt;afplay&lt;/code&gt; with &lt;code&gt;aplay&lt;/code&gt; or &lt;code&gt;paplay&lt;/code&gt;. On Windows, use &lt;code&gt;powershell -c &quot;(New-Object Media.SoundPlayer &apos;$env:USERPROFILE\.claude\hooks\ra_building.wav&apos;).PlaySync()&quot;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;Voice-per-session version&lt;/h2&gt;

&lt;p&gt;Instead of the same EVA sounds every time, you can give each session a random character. A voice script picks a character on session start and saves it to a temp file. All subsequent hooks in that session read the file and play sounds for that character.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;~/.claude/hooks/voice.sh&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;#!/bin/bash
# Usage: voice.sh &amp;lt;event&amp;gt;
# Events: session_start, prompt_submit, stop, notification

HOOKS_DIR=~/.claude/hooks
MUTE_FILE=/tmp/ra-mute
PANE_ID=&quot;${TMUX_PANE#%}&quot;
VOICE_FILE=&quot;/tmp/cc-voice${PANE_ID:+-$PANE_ID}&quot;
EVENT=&quot;$1&quot;

[ -f &quot;$MUTE_FILE&quot; ] &amp;amp;&amp;amp; exit 0

play_random() {
  local sounds=(&quot;$@&quot;)
  local pick=&quot;${sounds[$((RANDOM % ${#sounds[@]}))]}&quot;
  afplay &quot;$HOOKS_DIR/$pick&quot; &amp;amp;
}

CHARACTERS=(eva allied_infantry_1 allied_infantry_2
  allied_vehicle_1 allied_vehicle_2 engineer medic spy)

if [ &quot;$EVENT&quot; = &quot;session_start&quot; ]; then
  CHAR=&quot;${CHARACTERS[$((RANDOM % ${#CHARACTERS[@]}))]}&quot;
  echo &quot;$CHAR&quot; &amp;gt; &quot;$VOICE_FILE&quot;
else
  [ -f &quot;$VOICE_FILE&quot; ] &amp;amp;&amp;amp; CHAR=$(cat &quot;$VOICE_FILE&quot;) || CHAR=&quot;eva&quot;
fi

case &quot;$CHAR&quot; in
  eva)
    case &quot;$EVENT&quot; in
      session_start) afplay &quot;$HOOKS_DIR/ra_new_construction_options.wav&quot; &amp;amp; ;;
      prompt_submit) afplay &quot;$HOOKS_DIR/ra_building.wav&quot; &amp;amp; ;;
      stop)          afplay &quot;$HOOKS_DIR/ra_construction_complete.wav&quot; &amp;amp; ;;
      notification)  afplay &quot;$HOOKS_DIR/ra_unit_ready.wav&quot; &amp;amp; ;;
    esac ;;
  allied_infantry_*)
    case &quot;$EVENT&quot; in
      session_start) play_random &quot;ra_yes_sir_${CHAR}.wav&quot; &quot;ra_ready_${CHAR}.wav&quot; ;;
      prompt_submit) play_random &quot;ra_acknowledged_${CHAR}.wav&quot; &quot;ra_affirmative_${CHAR}.wav&quot; &quot;ra_agreed_${CHAR}.wav&quot; &quot;ra_at_once_${CHAR}.wav&quot; ;;
      stop)          play_random &quot;ra_awaiting_orders_${CHAR}.wav&quot; &quot;ra_reporting_${CHAR}.wav&quot; ;;
      notification)  afplay &quot;$HOOKS_DIR/ra_ready_${CHAR}.wav&quot; &amp;amp; ;;
    esac ;;
  allied_vehicle_*)
    case &quot;$EVENT&quot; in
      session_start) afplay &quot;$HOOKS_DIR/ra_yes_sir_${CHAR}.wav&quot; &amp;amp; ;;
      prompt_submit) play_random &quot;ra_acknowledged_${CHAR}.wav&quot; &quot;ra_affirmative_${CHAR}.wav&quot; ;;
      stop)          play_random &quot;ra_awaiting_orders_${CHAR}.wav&quot; &quot;ra_reporting_${CHAR}.wav&quot; ;;
      notification)  afplay &quot;$HOOKS_DIR/ra_vehicle_reporting_${CHAR}.wav&quot; &amp;amp; ;;
    esac ;;
  engineer)
    case &quot;$EVENT&quot; in
      session_start) afplay &quot;$HOOKS_DIR/ra_engineer_yes_sir.wav&quot; &amp;amp; ;;
      prompt_submit) play_random ra_engineer_affirmative.wav ra_engineer_movin_out.wav ;;
      stop)          afplay &quot;$HOOKS_DIR/ra_engineer_engineering.wav&quot; &amp;amp; ;;
      notification)  afplay &quot;$HOOKS_DIR/ra_engineer_engineering.wav&quot; &amp;amp; ;;
    esac ;;
  medic)
    case &quot;$EVENT&quot; in
      session_start) afplay &quot;$HOOKS_DIR/ra_medic_yes_sir.wav&quot; &amp;amp; ;;
      prompt_submit) play_random ra_medic_affirmative.wav ra_medic_movin_out.wav ;;
      stop)          afplay &quot;$HOOKS_DIR/ra_medic_reporting.wav&quot; &amp;amp; ;;
      notification)  afplay &quot;$HOOKS_DIR/ra_medic_reporting.wav&quot; &amp;amp; ;;
    esac ;;
  spy)
    case &quot;$EVENT&quot; in
      session_start) afplay &quot;$HOOKS_DIR/ra_spy_for_king_and_country.wav&quot; &amp;amp; ;;
      prompt_submit) play_random ra_spy_indeed.wav ra_spy_yes_sir.wav ra_spy_on_my_way.wav ;;
      stop)          afplay &quot;$HOOKS_DIR/ra_spy_commander.wav&quot; &amp;amp; ;;
      notification)  afplay &quot;$HOOKS_DIR/ra_spy_indeed.wav&quot; &amp;amp; ;;
    esac ;;
esac&lt;/pre&gt;

&lt;p&gt;Make it executable:&lt;/p&gt;

&lt;pre&gt;chmod +x ~/.claude/hooks/voice.sh&lt;/pre&gt;

&lt;p&gt;Then point your hooks at the script instead of calling &lt;code&gt;afplay&lt;/code&gt; directly:&lt;/p&gt;

&lt;pre&gt;{
  &quot;hooks&quot;: {
    &quot;SessionStart&quot;: [
      {
        &quot;hooks&quot;: [
          { &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/voice.sh session_start &amp;amp;&quot; },
          { &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/pane-name.sh session_start &amp;amp;&quot; }
        ]
      }
    ],
    &quot;UserPromptSubmit&quot;: [
      {
        &quot;hooks&quot;: [
          { &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/voice.sh prompt_submit &amp;amp;&quot; },
          { &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/pane-name.sh prompt_submit &amp;amp;&quot; }
        ]
      }
    ],
    &quot;Stop&quot;: [
      {
        &quot;hooks&quot;: [
          { &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/voice.sh stop &amp;amp;&quot; },
          { &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/pane-name.sh stop &amp;amp;&quot; }
        ]
      }
    ],
    &quot;Notification&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash ~/.claude/hooks/voice.sh notification &amp;amp;&quot; }] }
    ],
    &quot;PreCompact&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;[ ! -f /tmp/ra-mute ] &amp;amp;&amp;amp; afplay ~/.claude/hooks/ra_low_power.wav &amp;amp;&quot; }] }
    ],
    &quot;PostToolUseFailure&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;[ ! -f /tmp/ra-mute ] &amp;amp;&amp;amp; afplay ~/.claude/hooks/ra_unable_to_build.wav &amp;amp;&quot; }] }
    ],
    &quot;SubagentStart&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;[ ! -f /tmp/ra-mute ] &amp;amp;&amp;amp; afplay ~/.claude/hooks/ra_reinforcements.wav &amp;amp;&quot; }] }
    ]
  }
}&lt;/pre&gt;

&lt;p&gt;The character roster: EVA (the base announcer), Allied Infantry (two voice actors), Allied Vehicle (two voice actors), Engineer, Medic, and Spy. Each character has sounds mapped to all four voice events&amp;mdash;greetings for session start, acknowledgments for prompt submit, status lines for stop, and alerts for notifications. Add or remove characters by editing the &lt;code&gt;CHARACTERS&lt;/code&gt; array and adding a case block.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pane-name.sh&lt;/code&gt; script runs alongside &lt;code&gt;voice.sh&lt;/code&gt; on session start, prompt submit, and stop. It assigns each tmux pane a random two-word name and keeps the title updated. The &lt;code&gt;PreCompact&lt;/code&gt; hook plays EVA&apos;s &amp;ldquo;Low power&amp;rdquo; warning when Claude is about to compact its context window&amp;mdash;a heads-up that the session is running long.&lt;/p&gt;

&lt;p&gt;The voice file is per-pane (&lt;code&gt;/tmp/cc-voice-{PANE_ID}&lt;/code&gt;), so multiple concurrent sessions each keep their own character. If you&apos;re not using tmux, it falls back to a single shared file.&lt;/p&gt;

&lt;h2&gt;Pane naming&lt;/h2&gt;

&lt;p&gt;If you run multiple sessions in tmux, they need names. A second hook script assigns each session a random two-word codename&amp;mdash;like &amp;ldquo;sharp-gate&amp;rdquo; or &amp;ldquo;cool-slab&amp;rdquo;&amp;mdash;and sets it as the tmux pane title. The name persists for the session. It only sets the pane title, not the window/tab name&amp;mdash;you can name your tabs however you want.&lt;/p&gt;

&lt;p&gt;You can set a topic from inside Claude Code by running &lt;code&gt;bash ~/.claude/hooks/pane-name.sh topic &quot;what I&apos;m working on&quot;&lt;/code&gt;, which updates the title to &amp;ldquo;sharp-gate - what I&apos;m working on&amp;rdquo;. Or add an instruction to &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; so Claude sets it when you ask:&lt;/p&gt;

&lt;pre&gt;## Pane naming
When I say &quot;name the pane&quot; or &quot;rename pane&quot;, run:
bash ~/.claude/hooks/pane-name.sh topic &quot;short topic description&quot;&lt;/pre&gt;

&lt;p&gt;The agent can read its own name from &lt;code&gt;/tmp/cc-pane-name-{PANE_ID}&lt;/code&gt;&amp;mdash;line 1 is the name, line 2 is the timestamp and topic.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;~/.claude/hooks/pane-name.sh&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;#!/bin/bash
# Each session gets a persistent adjective-noun name
# Usage:
#   pane-name.sh session_start          &amp;mdash; assign a random name
#   pane-name.sh prompt_submit|stop     &amp;mdash; re-assert current name
#   pane-name.sh topic &quot;short topic&quot;    &amp;mdash; set a session topic

PANE_ID=&quot;${TMUX_PANE:-unknown}&quot;
PANE_ID_SAFE=&quot;${PANE_ID#%}&quot;
NAME_FILE=&quot;/tmp/cc-pane-name-${PANE_ID_SAFE}&quot;
EVENT=&quot;$1&quot;

ADJECTIVES=(
  bright calm clear cool crisp dark deep dry fast firm
  fresh gold green grey iron keen light live long mild
  pale pine raw red salt sharp slow soft still warm
  wide wild young cold bold
)

NOUNS=(
  arch bay beam bolt cave cliff coast cove creek dawn
  dune edge flint forge gate glen grove helm hull knot
  lake ledge loft marsh mesa mist peak pier pine pond
  reef ridge root sand shore slab slope stone tide vale
  wall wave well wind yard
)

read_name_file() {
  if [ -f &quot;$NAME_FILE&quot; ]; then
    NAME=$(sed -n &apos;1p&apos; &quot;$NAME_FILE&quot;)
    TOPIC_LINE=$(sed -n &apos;2p&apos; &quot;$NAME_FILE&quot;)
  else
    NAME=&quot;claude&quot;
    TOPIC_LINE=&quot;&quot;
  fi
}

build_title() {
  if [ -n &quot;$TOPIC_LINE&quot; ]; then
    TOPIC=$(echo &quot;$TOPIC_LINE&quot; | sed &apos;s/^[^&amp;mdash;]*&amp;mdash; //&apos;)
    TITLE=&quot;${NAME} - ${TOPIC}&quot;
  else
    TITLE=&quot;$NAME&quot;
  fi
}

apply_title() {
  if [ -n &quot;$TMUX&quot; ] &amp;amp;&amp;amp; [ &quot;$PANE_ID&quot; != &quot;unknown&quot; ]; then
    tmux select-pane -t &quot;$PANE_ID&quot; -T &quot;$TITLE&quot; 2&amp;gt;/dev/null || true
  fi
}

case &quot;$EVENT&quot; in
  session_start)
    ADJ=&quot;${ADJECTIVES[$((RANDOM % ${#ADJECTIVES[@]}))]}&quot;
    NOUN=&quot;${NOUNS[$((RANDOM % ${#NOUNS[@]}))]}&quot;
    NAME=&quot;${ADJ}-${NOUN}&quot;
    TIMESTAMP=$(date &quot;+%Y-%m-%d %a %l:%M%p&quot; | sed &apos;s/  / /&apos;)
    echo &quot;$NAME&quot; &amp;gt; &quot;$NAME_FILE&quot;
    echo &quot;${TIMESTAMP} &amp;mdash; &quot; &amp;gt;&amp;gt; &quot;$NAME_FILE&quot;
    TOPIC_LINE=&quot;&quot;
    build_title
    apply_title
    ;;
  topic)
    TOPIC_TEXT=&quot;$2&quot;
    if [ ! -f &quot;$NAME_FILE&quot; ]; then
      ADJ=&quot;${ADJECTIVES[$((RANDOM % ${#ADJECTIVES[@]}))]}&quot;
      NOUN=&quot;${NOUNS[$((RANDOM % ${#NOUNS[@]}))]}&quot;
      NAME=&quot;${ADJ}-${NOUN}&quot;
    else
      read_name_file
    fi
    TIMESTAMP=$(date &quot;+%Y-%m-%d %a %l:%M%p&quot; | sed &apos;s/  / /&apos;)
    echo &quot;$NAME&quot; &amp;gt; &quot;$NAME_FILE&quot;
    echo &quot;${TIMESTAMP} &amp;mdash; ${TOPIC_TEXT}&quot; &amp;gt;&amp;gt; &quot;$NAME_FILE&quot;
    TOPIC_LINE=&quot;${TIMESTAMP} &amp;mdash; ${TOPIC_TEXT}&quot;
    build_title
    apply_title
    ;;
  *)
    [ -f &quot;$NAME_FILE&quot; ] || exit 0
    read_name_file
    build_title
    apply_title
    ;;
esac&lt;/pre&gt;

&lt;p&gt;Make it executable:&lt;/p&gt;

&lt;pre&gt;chmod +x ~/.claude/hooks/pane-name.sh&lt;/pre&gt;

&lt;h2&gt;Muting&lt;/h2&gt;

&lt;p&gt;The voice script checks for a mute file before playing anything. If &lt;code&gt;/tmp/ra-mute&lt;/code&gt; exists, it exits silently. To mute and unmute:&lt;/p&gt;

&lt;pre&gt;touch /tmp/ra-mute   # mute
rm /tmp/ra-mute      # unmute&lt;/pre&gt;

&lt;p&gt;Or add a toggle alias to your &lt;code&gt;~/.zshrc&lt;/code&gt; (or &lt;code&gt;~/.bashrc&lt;/code&gt;):&lt;/p&gt;

&lt;pre&gt;alias ra=&apos;[ -f /tmp/ra-mute ] &amp;amp;&amp;amp; rm /tmp/ra-mute &amp;amp;&amp;amp; echo &quot;unmuted&quot; || (touch /tmp/ra-mute &amp;amp;&amp;amp; echo &quot;muted&quot;)&apos;&lt;/pre&gt;

&lt;p&gt;Then type &lt;code&gt;ra&lt;/code&gt; in any terminal to flip it. One mute file silences both Claude Code and &lt;a href=&quot;/red-alert-sounds-for-codex&quot;&gt;Codex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you&apos;re using the simple version (direct &lt;code&gt;afplay&lt;/code&gt; calls), add a mute check to each hook command:&lt;/p&gt;

&lt;pre&gt;&quot;command&quot;: &quot;[ ! -f /tmp/ra-mute ] &amp;amp;&amp;amp; afplay ~/.claude/hooks/ra_building.wav &amp;amp;&quot;&lt;/pre&gt;

&lt;p&gt;You can also teach Claude Code to mute itself. Add this to &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;## Sound effects
If I say &quot;mute&quot;, &quot;shh&quot;, or similar, run: touch /tmp/ra-mute
If I say &quot;unmute&quot;, &quot;speak&quot;, or &quot;sounds on&quot;, run: rm /tmp/ra-mute&lt;/pre&gt;

&lt;p&gt;Then just say &quot;shh&quot; in a Claude Code session and it&apos;ll mute the sounds. Say &quot;speak&quot; to bring them back. This only works in Claude Code&amp;mdash;Codex doesn&apos;t have a global instructions file, so use the terminal alias there.&lt;/p&gt;

&lt;h2&gt;Picking sounds&lt;/h2&gt;

&lt;p&gt;The EVA lines map naturally to Claude Code events:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SessionStart:&lt;/strong&gt; &quot;New construction options&quot; is the obvious one. &quot;Reinforcements&quot; also works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UserPromptSubmit:&lt;/strong&gt; &quot;Building&quot; is the classic. Any of the infantry acknowledgments work too&amp;mdash;&quot;Acknowledged&quot;, &quot;Affirmative&quot;, &quot;Yes sir&quot; (in Allied or Soviet accents).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop:&lt;/strong&gt; &quot;Construction complete&quot; when a task finishes. &quot;Mission accomplished&quot; for a more dramatic version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notification:&lt;/strong&gt; &quot;Unit ready&quot; is clean and short. The Spy&apos;s &quot;For king and country&quot; or Tanya&apos;s &quot;Shake it baby&quot; if you want personality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PreCompact:&lt;/strong&gt; &quot;Low power&quot; when the context window fills up and compaction is about to trigger. A heads-up that the session is running long.&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_low_power.wav&quot;&gt;&amp;#9654; Low power&lt;/button&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostToolUseFailure:&lt;/strong&gt; &quot;Unable to build&quot; when a tool call fails. Infrequent enough to be useful, not annoying.&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_unable_to_build.wav&quot;&gt;&amp;#9654; Unable to build&lt;/button&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SubagentStart:&lt;/strong&gt; &quot;Reinforcements have arrived&quot; when Claude spawns a subagent via the Task tool.&lt;br&gt;
&lt;button class=&quot;sound-btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reinforcements.wav&quot;&gt;&amp;#9654; Reinforcements have arrived&lt;/button&gt;&lt;/p&gt;

&lt;p&gt;We tried &lt;code&gt;SessionEnd&lt;/code&gt; with &quot;Mission accomplished&quot; but removed it&amp;mdash;it fires on every subagent session end, not just the main session, so it goes off constantly.&lt;/p&gt;

&lt;p&gt;Listen to all 164 on the &lt;a href=&quot;/red-alert-soundboard&quot;&gt;soundboard&lt;/a&gt; and swap in whatever you want.&lt;/p&gt;

&lt;h2&gt;How the sounds were extracted&lt;/h2&gt;

&lt;p&gt;The original Red Alert stores its audio in Westwood Studios&apos; MIX archive format. The EVA voices are in &lt;code&gt;speech.mix&lt;/code&gt; (encrypted with RSA+Blowfish), the Allied infantry accents are in &lt;code&gt;allies.mix&lt;/code&gt;, and the Soviet accents are in &lt;code&gt;russian.mix&lt;/code&gt;. Inside each archive, the individual sounds are in Westwood&apos;s AUD format&amp;mdash;IMA ADPCM compressed at 22050Hz.&lt;/p&gt;

&lt;p&gt;I wrote a Python script to read the MIX headers (decrypting when necessary), hash filenames using Westwood&apos;s rolling hash to look up entries, decode the AUD chunks, and write standard WAV files. The format details came from the &lt;a href=&quot;https://github.com/OpenRA/OpenRA&quot;&gt;OpenRA&lt;/a&gt; and &lt;a href=&quot;https://github.com/electronicarts/CnC_Remastered_Collection&quot;&gt;C&amp;amp;C Remastered Collection&lt;/a&gt; open source code.&lt;/p&gt;

&lt;p&gt;The trickiest part was the encrypted speech archive. The MIX header is encrypted with Blowfish, and the Blowfish key is itself encrypted with RSA. I ported OpenRA&apos;s C# bignum RSA decryption to Python, which gave me the 56-byte Blowfish key, which decrypted the header containing the file index. After that it was straightforward to look up entries by hash and decode the ADPCM audio.&lt;/p&gt;

&lt;p&gt;The infantry voices use a variation system where the same line has different accents depending on faction. Allied voices use file extensions &lt;code&gt;.V01&lt;/code&gt; and &lt;code&gt;.V03&lt;/code&gt;; Soviet voices use &lt;code&gt;.R01&lt;/code&gt; and &lt;code&gt;.R03&lt;/code&gt;. Vehicle crews get &lt;code&gt;.V00&lt;/code&gt;/&lt;code&gt;.V02&lt;/code&gt; and &lt;code&gt;.R00&lt;/code&gt;/&lt;code&gt;.R02&lt;/code&gt;. Each faction has two voice actors per line, selected by unit ID modulo 2.&lt;/p&gt;

&lt;p&gt;All 164 sounds are on the &lt;a href=&quot;/red-alert-soundboard&quot;&gt;soundboard&lt;/a&gt; and available as a &lt;a href=&quot;https://github.com/mgmobrien/mattobrien.org/releases/download/v1.0-sounds/ra-all-sounds.zip&quot;&gt;zip download&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/&quot;&gt;Home&lt;/a&gt;&lt;/p&gt;
&lt;script&gt;
let a=null;
document.querySelectorAll(&apos;.sound-btn&apos;).forEach(b=&gt;{
  b.addEventListener(&apos;click&apos;,()=&gt;{
    if(a){a.pause();a.currentTime=0;document.querySelectorAll(&apos;.sound-btn.playing&apos;).forEach(x=&gt;x.classList.remove(&apos;playing&apos;))}
    const s=new Audio(b.dataset.file);a=s;b.classList.add(&apos;playing&apos;);s.play();
    s.onended=()=&gt;{b.classList.remove(&apos;playing&apos;);a=null};
  });
});
&lt;/script&gt;</content>
  </entry>
  <entry>
    <title>Red Alert Soundboard</title>
    <link href="https://mattobrien.org/red-alert-soundboard/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/red-alert-soundboard/</id>
    <published>2026-02-09T00:00:00.000Z</published>
    <updated>2026-02-09T00:00:00.000Z</updated>
    <summary>164 Command &amp; Conquer: Red Alert EVA announcements and unit voices extracted from original Westwood MIX/AUD game files. Free download. Use them as Claude Code hooks.</summary>
    <content type="html">&lt;div class=&quot;site-header&quot;&gt;&lt;a href=&quot;/&quot;&gt;&amp;larr; mattobrien.org&lt;/a&gt;&lt;/div&gt;

&lt;h1&gt;Red Alert Soundboard&lt;/h1&gt;
&lt;div class=&quot;subtitle&quot;&gt;Command &amp;amp; Conquer: Red Alert &amp;middot; EVA &amp;amp; Unit Voices&lt;/div&gt;

&lt;div id=&quot;now-playing&quot;&gt;&lt;/div&gt;

&lt;div class=&quot;controls&quot;&gt;
  &lt;button onclick=&quot;stopAll()&quot;&gt;Stop&lt;/button&gt;
  &lt;button onclick=&quot;playRandom()&quot;&gt;Random&lt;/button&gt;
&lt;/div&gt;

&lt;div class=&quot;section eva&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;EVA Announcements&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_building.wav&quot;&gt;Building&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_construction_complete.wav&quot;&gt;Construction complete&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_new_construction_options.wav&quot;&gt;New construction options&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_unit_ready.wav&quot;&gt;Unit ready&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reinforcements.wav&quot;&gt;Reinforcements&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_insufficient_funds.wav&quot;&gt;Insufficient funds&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_low_power.wav&quot;&gt;Low power&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_base_under_attack.wav&quot;&gt;Base under attack&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_structure_destroyed.wav&quot;&gt;Structure destroyed&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_unable_to_build.wav&quot;&gt;Unable to build&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_primary_building_selected.wav&quot;&gt;Primary building selected&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_canceled.wav&quot;&gt;Canceled&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mission_accomplished.wav&quot;&gt;Mission accomplished&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mission_failed.wav&quot;&gt;Mission failed&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section allied&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Allied Infantry&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_allied_infantry_1.wav&quot;&gt;Yes sir 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_allied_infantry_2.wav&quot;&gt;Yes sir 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_allied_infantry_1.wav&quot;&gt;Acknowledged 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_allied_infantry_2.wav&quot;&gt;Acknowledged 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_allied_infantry_1.wav&quot;&gt;Affirmative 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_allied_infantry_2.wav&quot;&gt;Affirmative 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_allied_infantry_1.wav&quot;&gt;Awaiting orders 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_allied_infantry_2.wav&quot;&gt;Awaiting orders 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_allied_infantry_1.wav&quot;&gt;Reporting 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_allied_infantry_2.wav&quot;&gt;Reporting 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_ready_allied_infantry_1.wav&quot;&gt;Ready 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_ready_allied_infantry_2.wav&quot;&gt;Ready 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_agreed_allied_infantry_1.wav&quot;&gt;Agreed 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_agreed_allied_infantry_2.wav&quot;&gt;Agreed 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_at_once_allied_infantry_1.wav&quot;&gt;At once 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_at_once_allied_infantry_2.wav&quot;&gt;At once 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_of_course_allied_infantry_1.wav&quot;&gt;Of course 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_of_course_allied_infantry_2.wav&quot;&gt;Of course 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_very_well_allied_infantry_1.wav&quot;&gt;Very well 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_very_well_allied_infantry_2.wav&quot;&gt;Very well 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_as_you_wish_allied_infantry_1.wav&quot;&gt;As you wish 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_as_you_wish_allied_infantry_2.wav&quot;&gt;As you wish 2&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section allied&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Allied Vehicles&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_allied_vehicle_1.wav&quot;&gt;Yes sir 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_allied_vehicle_2.wav&quot;&gt;Yes sir 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_allied_vehicle_1.wav&quot;&gt;Acknowledged 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_allied_vehicle_2.wav&quot;&gt;Acknowledged 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_allied_vehicle_1.wav&quot;&gt;Affirmative 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_allied_vehicle_2.wav&quot;&gt;Affirmative 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_allied_vehicle_1.wav&quot;&gt;Awaiting orders 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_allied_vehicle_2.wav&quot;&gt;Awaiting orders 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_allied_vehicle_1.wav&quot;&gt;Reporting 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_allied_vehicle_2.wav&quot;&gt;Reporting 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_vehicle_reporting_allied_vehicle_1.wav&quot;&gt;Vehicle reporting 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_vehicle_reporting_allied_vehicle_2.wav&quot;&gt;Vehicle reporting 2&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section soviet&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Soviet Infantry&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_soviet_infantry_1.wav&quot;&gt;Yes sir 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_soviet_infantry_2.wav&quot;&gt;Yes sir 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_soviet_infantry_1.wav&quot;&gt;Acknowledged 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_soviet_infantry_2.wav&quot;&gt;Acknowledged 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_soviet_infantry_1.wav&quot;&gt;Affirmative 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_soviet_infantry_2.wav&quot;&gt;Affirmative 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_infantry_1.wav&quot;&gt;Awaiting orders 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_infantry_2.wav&quot;&gt;Awaiting orders 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_soviet_infantry_1.wav&quot;&gt;Reporting 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_soviet_infantry_2.wav&quot;&gt;Reporting 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_ready_soviet_infantry_1.wav&quot;&gt;Ready 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_ready_soviet_infantry_2.wav&quot;&gt;Ready 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_agreed_soviet_infantry_1.wav&quot;&gt;Agreed 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_agreed_soviet_infantry_2.wav&quot;&gt;Agreed 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_at_once_soviet_infantry_1.wav&quot;&gt;At once 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_at_once_soviet_infantry_2.wav&quot;&gt;At once 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_of_course_soviet_infantry_1.wav&quot;&gt;Of course 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_of_course_soviet_infantry_2.wav&quot;&gt;Of course 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_very_well_soviet_infantry_1.wav&quot;&gt;Very well 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_very_well_soviet_infantry_2.wav&quot;&gt;Very well 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_as_you_wish_soviet_infantry_1.wav&quot;&gt;As you wish 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_as_you_wish_soviet_infantry_2.wav&quot;&gt;As you wish 2&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section soviet&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Soviet Vehicles&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_soviet_vehicle_1.wav&quot;&gt;Yes sir 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_yes_sir_soviet_vehicle_2.wav&quot;&gt;Yes sir 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_soviet_vehicle_1.wav&quot;&gt;Acknowledged 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_acknowledged_soviet_vehicle_2.wav&quot;&gt;Acknowledged 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_soviet_vehicle_1.wav&quot;&gt;Affirmative 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_affirmative_soviet_vehicle_2.wav&quot;&gt;Affirmative 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_vehicle_1.wav&quot;&gt;Awaiting orders 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_awaiting_orders_soviet_vehicle_2.wav&quot;&gt;Awaiting orders 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_soviet_vehicle_1.wav&quot;&gt;Reporting 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_reporting_soviet_vehicle_2.wav&quot;&gt;Reporting 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_vehicle_reporting_soviet_vehicle_1.wav&quot;&gt;Vehicle reporting 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_vehicle_reporting_soviet_vehicle_2.wav&quot;&gt;Vehicle reporting 2&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section units&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Special Units&lt;/h2&gt;&lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Tanya&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_yes_sir.wav&quot;&gt;Yes sir&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_shake_it_baby.wav&quot;&gt;Shake it baby&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_cha_ching.wav&quot;&gt;Cha-ching&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_chew_on_this.wav&quot;&gt;Chew on this&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_lets_rock.wav&quot;&gt;Let&apos;s rock&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_give_it_to_me.wav&quot;&gt;Give it to me&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_im_there.wav&quot;&gt;I&apos;m there&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_kiss_it_bye_bye.wav&quot;&gt;Kiss it bye bye&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_thats_all_you_got.wav&quot;&gt;That&apos;s all you got&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_whats_up.wav&quot;&gt;What&apos;s up&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_yea.wav&quot;&gt;Yea!&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tanya_laugh.wav&quot;&gt;Laugh&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Spy&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_yes_sir.wav&quot;&gt;Yes sir&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_for_king_and_country.wav&quot;&gt;For king and country&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_commander.wav&quot;&gt;Commander&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_indeed.wav&quot;&gt;Indeed&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_spy_on_my_way.wav&quot;&gt;On my way&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Engineer&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_engineer_yes_sir.wav&quot;&gt;Yes sir&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_engineer_affirmative.wav&quot;&gt;Affirmative&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_engineer_engineering.wav&quot;&gt;Engineering&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_engineer_movin_out.wav&quot;&gt;Movin&apos; out&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Medic&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_medic_yes_sir.wav&quot;&gt;Yes sir&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_medic_affirmative.wav&quot;&gt;Affirmative&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_medic_reporting.wav&quot;&gt;Reporting&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_medic_movin_out.wav&quot;&gt;Movin&apos; out&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Mechanic&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_yes_sir.wav&quot;&gt;Yes sir&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_sure_thing_boss.wav&quot;&gt;Sure thing boss&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_howdy.wav&quot;&gt;Howdy&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_i_hear_ya.wav&quot;&gt;I hear ya&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_ill_get_my_wrench.wav&quot;&gt;I&apos;ll get my wrench&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_hot_diggity.wav&quot;&gt;Hot diggity&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_rise_n_shine.wav&quot;&gt;Rise &apos;n shine&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_yee_haw.wav&quot;&gt;Yee haw&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_guffaw.wav&quot;&gt;Guffaw&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_mechanic_huh.wav&quot;&gt;Huh&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Shock Trooper&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_fully_charged.wav&quot;&gt;Fully charged&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_shocking.wav&quot;&gt;Shocking&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_burn_baby_burn.wav&quot;&gt;Burn baby burn&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_extra_crispy.wav&quot;&gt;Extra crispy&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_lets_dance.wav&quot;&gt;Let&apos;s dance&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_lights_out.wav&quot;&gt;Lights out&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_got_juice.wav&quot;&gt;Got juice&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_need_a_jump.wav&quot;&gt;Need a jump&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_power_on.wav&quot;&gt;Power on&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_shock_yesssss.wav&quot;&gt;Yesssss&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Thief&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_thief_affirmative.wav&quot;&gt;Affirmative&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_thief_movin_out.wav&quot;&gt;Movin&apos; out&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_thief_ok.wav&quot;&gt;Ok&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_thief_what.wav&quot;&gt;What&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_thief_yea.wav&quot;&gt;Yea&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Dog&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_dog_yes_sir.wav&quot;&gt;Yes sir (bark)&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_dog_bark.wav&quot;&gt;Bark&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_dog_growl.wav&quot;&gt;Growl&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_dog_whine.wav&quot;&gt;Whine&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_dog_hurt.wav&quot;&gt;Hurt&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;unit-group&quot;&gt;
    &lt;div class=&quot;unit-group-label&quot;&gt;Other&lt;/div&gt;
    &lt;div class=&quot;grid&quot;&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tesla_charge_up.wav&quot;&gt;Tesla charge up&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_tesla_zap.wav&quot;&gt;Tesla zap&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_chronosphere_sound.wav&quot;&gt;Chronosphere&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_minelay1.wav&quot;&gt;Mine layer&lt;/button&gt;
      &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_buzzy1.wav&quot;&gt;Buzzy&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section death&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Death Sounds&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman1.wav&quot;&gt;Death 1&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman2.wav&quot;&gt;Death 2&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman3.wav&quot;&gt;Death 3&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman4.wav&quot;&gt;Death 4&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman5.wav&quot;&gt;Death 5&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman6.wav&quot;&gt;Death 6&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman7.wav&quot;&gt;Death 7&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman8.wav&quot;&gt;Death 8&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/ra_death_dedman10.wav&quot;&gt;Death 10&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;section td&quot;&gt;
  &lt;div class=&quot;section-header&quot;&gt;&lt;h2&gt;Tiberian Dawn EVA&lt;/h2&gt;&lt;/div&gt;
  &lt;div class=&quot;grid&quot;&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_building.wav&quot;&gt;Building&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_construction_complete.wav&quot;&gt;Construction complete&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_new_construction_options.wav&quot;&gt;New construction options&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_unit_ready.wav&quot;&gt;Unit ready&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_unit_lost.wav&quot;&gt;Unit lost&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_base_under_attack.wav&quot;&gt;Base under attack&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_low_power.wav&quot;&gt;Low power&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_mission_accomplished.wav&quot;&gt;Mission accomplished&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_mission_failed.wav&quot;&gt;Mission failed&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_building_progress.wav&quot;&gt;Building (progress)&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_canceled.wav&quot;&gt;Canceled&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_on_hold.wav&quot;&gt;On hold&lt;/button&gt;
    &lt;button class=&quot;btn&quot; data-file=&quot;https://mgmobrien.github.io/red-alert-sounds/td_select_target.wav&quot;&gt;Select target&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;about&quot;&gt;
  &lt;h3&gt;Download&lt;/h3&gt;
  &lt;p&gt;&lt;a href=&quot;https://github.com/mgmobrien/mattobrien.org/releases/download/v1.0-sounds/ra-all-sounds.zip&quot;&gt;Download all 164 sounds&lt;/a&gt; (5.7 MB zip)&lt;/p&gt;

  &lt;h3&gt;Use as Claude Code hooks&lt;/h3&gt;
  &lt;p&gt;
    &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;Claude Code hooks&lt;/a&gt; let you play sounds when events fire.
    Download the zip, unzip into &lt;code&gt;~/.claude/hooks/&lt;/code&gt;, and add this to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:
  &lt;/p&gt;
  &lt;pre&gt;{
  &quot;hooks&quot;: {
    &quot;SessionStart&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;,
          &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_new_construction_options.wav &amp;&quot; }] }
    ],
    &quot;UserPromptSubmit&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;,
          &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_building.wav &amp;&quot; }] }
    ],
    &quot;Stop&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;,
          &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_construction_complete.wav &amp;&quot; }] }
    ],
    &quot;Notification&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;,
          &quot;command&quot;: &quot;afplay ~/.claude/hooks/ra_unit_ready.wav &amp;&quot; }] }
    ]
  }
}&lt;/pre&gt;
  &lt;p&gt;On Linux, replace &lt;code&gt;afplay&lt;/code&gt; with &lt;code&gt;aplay&lt;/code&gt; or &lt;code&gt;paplay&lt;/code&gt;. On Windows, use &lt;code&gt;powershell -c (New-Object Media.SoundPlayer &apos;path\\to\\file.wav&apos;).PlaySync()&lt;/code&gt;.&lt;/p&gt;
  &lt;p&gt;Swap in any sound you want. The EVA lines map well to session events; the infantry voices work for notifications.&lt;/p&gt;

  &lt;h3&gt;About&lt;/h3&gt;
  &lt;p&gt;
    164 sounds extracted from the original Command &amp;amp; Conquer: Red Alert (1996) and C&amp;amp;C: Tiberian Dawn (1995) game files.
    Audio decoded from Westwood Studios MIX/AUD archives (including RSA+Blowfish encrypted headers) using custom Python referencing the &lt;a href=&quot;https://github.com/OpenRA/OpenRA&quot;&gt;OpenRA&lt;/a&gt; and &lt;a href=&quot;https://github.com/electronicarts/CnC_Remastered_Collection&quot;&gt;C&amp;amp;C Remastered&lt;/a&gt; source code.
    Inspired by &lt;a href=&quot;https://x.com/delba_oliveira/status/2020515010985005255&quot;&gt;Delba&apos;s post&lt;/a&gt; about using game sounds as Claude Code hooks.
  &lt;/p&gt;
  &lt;p&gt;Space to stop, R for random.&lt;/p&gt;
  &lt;p&gt;&lt;a href=&quot;/&quot;&gt;Matt O&apos;Brien&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;script&gt;
let currentAudio = null;
let currentBtn = null;

function stopAll() {
  if (currentAudio) {
    currentAudio.pause();
    currentAudio.currentTime = 0;
    currentAudio = null;
  }
  if (currentBtn) {
    currentBtn.classList.remove(&apos;playing&apos;);
    currentBtn = null;
  }
  document.getElementById(&apos;now-playing&apos;).textContent = &apos;&apos;;
}

function playSound(btn) {
  const file = btn.dataset.file;
  stopAll();
  const audio = new Audio(file);
  currentAudio = audio;
  currentBtn = btn;
  btn.classList.add(&apos;playing&apos;);
  document.getElementById(&apos;now-playing&apos;).textContent = file.split(&apos;/&apos;).pop();
  audio.play();
  audio.onended = () =&gt; {
    btn.classList.remove(&apos;playing&apos;);
    currentBtn = null;
    currentAudio = null;
    document.getElementById(&apos;now-playing&apos;).textContent = &apos;&apos;;
  };
}

function playRandom() {
  const buttons = document.querySelectorAll(&apos;.btn[data-file]&apos;);
  const btn = buttons[Math.floor(Math.random() * buttons.length)];
  playSound(btn);
  btn.scrollIntoView({ behavior: &apos;smooth&apos;, block: &apos;center&apos; });
}

document.querySelectorAll(&apos;.btn[data-file]&apos;).forEach(btn =&gt; {
  btn.addEventListener(&apos;click&apos;, () =&gt; playSound(btn));
});

document.addEventListener(&apos;keydown&apos;, e =&gt; {
  if (e.code === &apos;Space&apos;) { e.preventDefault(); stopAll(); }
  if (e.code === &apos;KeyR&apos; &amp;&amp; !e.metaKey &amp;&amp; !e.ctrlKey) { e.preventDefault(); playRandom(); }
});
&lt;/script&gt;</content>
  </entry>
  <entry>
    <title>Douglas Engelbart&apos;s vision (1962): augmenting human intellect</title>
    <link href="https://mattobrien.org/engelbarts-vision-1962/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/engelbarts-vision-1962/</id>
    <published>2025-12-19T06:21:08.000Z</published>
    <updated>2025-12-19T06:21:08.000Z</updated>
    <summary>Engelbart saw computers as tools to increase human capability. His 1962 report laid out a vision of human-computer partnership.</summary>
    <content type="html">&lt;p&gt;In 1962, computers were widely viewed as calculation engines—tools for automation. Douglas Engelbart viewed them as tools to increase human capability in the face of complex problems.&lt;/p&gt;

&lt;p&gt;He laid out his vision in his seminal report, &lt;em&gt;Augmenting Human Intellect: A Conceptual Framework&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&quot;By &apos;augmenting man&apos;s intellect&apos; we mean increasing the capability of a man to approach a complex problem situation, to gain comprehension to suit his particular needs, and to derive solutions to problems.&lt;/p&gt;
&lt;p&gt;... We do not speak of isolated clever tricks that help in particular situations. We refer to a way of life in an integrated domain where hunches, cut-and-try, intangibles, and the human &apos;feel for a situation&apos; usefully coexist with powerful concepts, streamlined technology and notation, sophisticated methods, and high-powered electronic aids.&quot;&lt;sup&gt;&lt;a href=&quot;#fn1&quot; id=&quot;ref1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p class=&quot;article-date footnote-back&quot; id=&quot;fn1&quot;&gt;&lt;sup&gt;1&lt;/sup&gt; Douglas Engelbart, &lt;a href=&quot;https://www.dougengelbart.org/pubs/augment-3906.html&quot;&gt;&lt;em&gt;Augmenting Human Intellect: A Conceptual Framework&lt;/em&gt;&lt;/a&gt;, SRI Summary Report AFOSR-3223 (1962). Selection via M. Mitchell Waldrop, &lt;em&gt;The Dream Machine&lt;/em&gt; (2001), p. 212. &lt;a href=&quot;#ref1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The cost of learning to program is no longer front-loaded</title>
    <link href="https://mattobrien.org/the-cost-of-learning-to-program-is-no-longer-front-loaded/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/the-cost-of-learning-to-program-is-no-longer-front-loaded/</id>
    <published>2025-12-18T08:10:33.000Z</published>
    <updated>2025-12-18T08:10:33.000Z</updated>
    <summary>Learning to program used to require large upfront investment before any payoff. Language models changed the shape of the curve - you get utility right away. As a result, many more people will write software.</summary>
    <content type="html">&lt;p&gt;Learning to program used to be like learning to play the saxophone.&lt;/p&gt;

&lt;p&gt;You want to make beautiful sounds. But with a reed instrument, even making your first &lt;em&gt;note&lt;/em&gt; requires practice and mouth-strength. It might be a day&apos;s work before you can get anything other than squeaks and hisses out of your instrument. Weeks before it reliably sounds decent. The learning curve is punishing upfront - massive investment for zero output.&lt;/p&gt;

&lt;pre&gt;
THE SAXOPHONE LEARNING CURVE

Payoff
  ^
  |                     / (Music)
  |                    /
  |                   /
  |__________________/
  | (Squeaking)
  +-------------------------&gt; Time practicing
&lt;/pre&gt;

&lt;p&gt;Programming was the same. You want to make cool and useful stuff. But first: environment setup, Stack Overflow, arcane decisions, syntax, regret about life choices, terminal commands. It might be hours before you first get to &quot;hello world&quot; and weeks before you get anything useful.&lt;/p&gt;

&lt;pre&gt;
THE OLD PROGRAMMING LEARNING CURVE

Payoff
  ^
  |                     / (Utility)
  |                    /
  |                   /
  |__________________/
  | (Syntax &amp; Config)
  +-------------------------&gt; Time
&lt;/pre&gt;

&lt;p&gt;Language models changed the shape of the curve. Now you can describe what you want and get working code instantly. Not perfect, clean, production code - but perfectly serviceable, interactive prototype, proof-of-concept kind of code. Instantly recognizable music.&lt;/p&gt;

&lt;pre&gt;
THE AI PROGRAMMING LEARNING CURVE

Payoff
  ^
  |             .......... (The Last Mile Slog)
  |         _ /
  |     _ /
  |  _/       (Utility)
  | /
  |/ (Instant prototype)
  |
  +-------------------------&gt; Time
&lt;/pre&gt;

&lt;p&gt;It&apos;s fucking fun.&lt;/p&gt;

&lt;p&gt;This isn&apos;t to say &quot;AI makes programming easy.&quot; There is still a learning curve. You still need to understand what you&apos;re building. There&apos;s still great art and science required to build successful product.&lt;/p&gt;

&lt;p&gt;But the &lt;em&gt;transactional order&lt;/em&gt; is different. You get utility (a working tool) before you pay the full cost (fluency).&lt;/p&gt;

&lt;p&gt;It&apos;s a reduction in time to first payoff. When first payoff comes after months of investment, only the highly motivated push through. When payoff comes immediately, more people get paid, and thus stick with it, and thus climb the curve.&lt;/p&gt;

&lt;p&gt;In 1957, FORTRAN - the first widely-used high-level language - let scientists write programs without learning machine code first. It &quot;enlarged the user base by a factor of ten, at least.&quot;&lt;sup&gt;&lt;a href=&quot;#fn1&quot; id=&quot;ref1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; I think we&apos;re seeing the same pattern here. When you collapse the upfront cost of a valuable tool, you explode the number of people using it. A lot more people are becoming programmers.&lt;/p&gt;

&lt;p class=&quot;article-date footnote-back&quot; id=&quot;fn1&quot;&gt;&lt;sup&gt;1&lt;/sup&gt; Fernando Corbató, quoted in M. Mitchell Waldrop, &lt;em&gt;The Dream Machine&lt;/em&gt; (2001), pp. 169-170. &lt;a href=&quot;#ref1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Etymology of sovereignty</title>
    <link href="https://mattobrien.org/etymology-of-sovereignty/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/etymology-of-sovereignty/</id>
    <published>2025-12-12T05:34:28.000Z</published>
    <updated>2025-12-12T05:34:28.000Z</updated>
    <summary>Sovereign comes from Latin superanus — &apos;of that which is above&apos;. The meaning has evolved from &apos;authority over those below&apos; to &apos;freedom from any above&apos;.</summary>
    <content type="html">&lt;p&gt;&quot;Sovereign&quot; comes from Vulgar Latin &lt;em&gt;*superanus&lt;/em&gt;, meaning &quot;chief, principal.&quot;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Super&lt;/em&gt; means &quot;over&quot; in Latin (from PIE root &lt;em&gt;*uper&lt;/em&gt;, &quot;over&quot;).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;-ānus&lt;/em&gt; is a compound Latin suffix forming adjectives, meaning &quot;of, belonging to, connected with&quot;. It&apos;s a stem vowel &lt;em&gt;-ā-&lt;/em&gt; plus &lt;em&gt;-nus&lt;/em&gt;, which derives from PIE &lt;em&gt;*-no-&lt;/em&gt;. This suffix gives us:
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;urbānus&lt;/em&gt; (from &lt;em&gt;urbs&lt;/em&gt;, city: &quot;of the city&quot; → urban),&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Rōmānus&lt;/em&gt; (from &lt;em&gt;Rōma&lt;/em&gt;, Rome: &quot;of Rome&quot; → Roman), and&lt;/li&gt;
&lt;li&gt;&lt;em&gt;montānus&lt;/em&gt; (from &lt;em&gt;mōns&lt;/em&gt;, mountain: &quot;of the mountains&quot;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;em&gt;*superanus&lt;/em&gt; = &quot;of/belonging to that which is above.&quot;&lt;/p&gt;

&lt;p&gt;Italian connection: &lt;em&gt;soprano&lt;/em&gt; comes from the same Latin root. The highest voice - the one above.&lt;/p&gt;

&lt;p&gt;The word entered English in the late 13th century as &lt;em&gt;soverain&lt;/em&gt;, meaning &quot;superior, ruler, master, one who is superior to or has power over another.&quot; By c. 1300 it meant specifically &quot;a king or queen, one who exercises dominion over people.&quot;&lt;/p&gt;

&lt;p&gt;In his &lt;em&gt;Leviathan&lt;/em&gt; (1651), Thomas Hobbes abstracted the concept: his sovereign was not necessarily a person but an office - an artificial person representing the state. This move detached sovereignty from flesh-and-blood monarchs and attached it to the abstract commonwealth itself. Where previous thinkers saw a supreme king, Hobbes saw a function that could be filled by monarch, aristocracy, or democracy alike.&lt;/p&gt;

&lt;p&gt;By the 18th century, sovereignty shifted to mean supreme authority &lt;em&gt;within a territory&lt;/em&gt;, with multiple sovereigns recognizing each other as equals. Many that are highest, with none above - but not each claiming to be higher than all the others. (The Peace of Westphalia in 1648 is often credited with establishing this system, though scholars note this is partly a retcon - the treaties never mention sovereignty, and the concept developed gradually.)&lt;/p&gt;

&lt;p&gt;In the 21st century the concept evolved further. &quot;Data sovereignty&quot; asserts national jurisdiction over information within borders - Westphalian logic applied to servers. &quot;Self-sovereign identity&quot; in crypto circles sees the individual as their own jurisdiction, holding keys no authority can revoke.&lt;/p&gt;

&lt;p&gt;The semantic core has shifted from emphasizing authority over those below to emphasizing freedom from any above.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;small&gt;Footnote: The asterisk in eg *superanus* marks it as a reconstructed form - a word that linguists believe existed but isn&apos;t found in surviving texts. Classical Latin texts don&apos;t contain this word, but linguists reconstruct it because the Romance language cognates (French souverain, Spanish soberano, Italian sovrano) all point back to this form, and it follows regular Latin word-formation patterns. The asterisks on PIE forms (*uper*, *-no-*) work the same way - reconstructed proto-forms that predate written records entirely.&lt;/small&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>There&apos;s a scene forming around Obsidian and local-first. What&apos;s it about?</title>
    <link href="https://mattobrien.org/scene-without-a-name/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/scene-without-a-name/</id>
    <published>2025-12-10T22:58:26.000Z</published>
    <updated>2025-12-10T22:58:26.000Z</updated>
    <summary>A scene is forming around Obsidian, local-first, and file-over-app. This mini-essay series will explore what it&apos;s about.</summary>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;Obsidian is around the state of the art of a philosophy of software and what it could be.&lt;/p&gt;
&lt;p&gt;—Andrej Karpathy, 2024&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have the sense that there is a &apos;&lt;a href=&quot;/scene&quot;&gt;scene&lt;/a&gt;&apos; forming around Obsidian and the file-over-app philosophy. I&apos;ve been trying to understand what it&apos;s about.&lt;/p&gt;

&lt;h2&gt;Who&apos;s in it:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Obsidian users building personal knowledge systems&lt;/li&gt;
&lt;li&gt;Local-first developers (Ink &amp; Switch, CRDTs, sync without central servers)&lt;/li&gt;
&lt;li&gt;Markdown and plain text advocates&lt;/li&gt;
&lt;li&gt;Software developers who have always insisted on data sovereignty and programmatic interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What they seem to share:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Files over constructed views of your data&lt;/li&gt;
&lt;li&gt;Possession of your data over permission to access it&lt;/li&gt;
&lt;li&gt;Open ecosystems over walled gardens&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/monolithic-vs-composable&quot;&gt;Composable systems over monoliths&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Open formats over proprietary ones&lt;/li&gt;
&lt;li&gt;Programmatic interfaces over graphical user interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What it&apos;s reacting against:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Cloud-era shift away from &apos;personal computing&apos; to thin clients, with software, storage, and compute happening on someone else&apos;s servers&lt;/li&gt;
&lt;li&gt;Appification - each app trying to own as much as it can, the individual subordinated to tenant status within platforms&lt;/li&gt;
&lt;li&gt;Enshittification - as lock-in grows, the gap between their interests and yours becomes visible, and they follow theirs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;In-the-neighborhood organizing principles:&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Local-first&lt;/strong&gt; - but &quot;local&quot; isn&apos;t quite it. Your own remote server can be sovereign. &lt;a href=&quot;/notion-is-simulated-composability&quot;&gt;Notion&apos;s offline mode is local but still a prison&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sovereignty&lt;/strong&gt; - owning your data and tools, not renting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Composability&lt;/strong&gt; - small tools that work together, &lt;a href=&quot;/unix-philosophy&quot;&gt;Unix philosophy&lt;/a&gt;. Files and text as universal interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Human augmentation done right&lt;/strong&gt; - tools that make you better at achieving satisfaction and actualization.&lt;/p&gt;

&lt;p&gt;None of these is quite the true center and organizing principle of the movement. The scene, if there is one, doesn&apos;t yet have a name and a central metaphor. But I think something is happening. That&apos;s what this set of mini-essays will be exploring.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Unix vs Unix-like vs POSIX</title>
    <link href="https://mattobrien.org/unix-vs-unix-like-vs-posix/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/unix-vs-unix-like-vs-posix/</id>
    <published>2025-11-28T22:15:11.000Z</published>
    <updated>2025-11-28T22:15:11.000Z</updated>
    <summary>Unix is a trademark. Unix-like is a set of characteristics. POSIX is the IEEE spec that codifies them.</summary>
    <content type="html">&lt;h2&gt;Unix&lt;/h2&gt;

        &lt;p&gt;&lt;strong&gt;Unix&lt;/strong&gt; today is a trademark. &lt;a href=&quot;/unix-scene&quot;&gt;Historically&lt;/a&gt; it was an operating system from AT&amp;T&apos;s Bell Labs (1969), then a family of operating systems descended from the original AT&amp;T Unix. Now, to legally call your OS &quot;Unix&quot;, you need trademark certification from The Open Group.&lt;/p&gt;

        &lt;p&gt;macOS is certified Unix.&lt;/p&gt;

        &lt;h2&gt;Unix-like&lt;/h2&gt;

        &lt;p&gt;&lt;strong&gt;Unix-like&lt;/strong&gt; means implementing the Unix interface:&lt;/p&gt;

        &lt;ul&gt;
            &lt;li&gt;Hierarchical file system rooted at /&lt;/li&gt;
            &lt;li&gt;Everything is a file (devices, sockets, pipes)&lt;/li&gt;
            &lt;li&gt;Text streams for input/output&lt;/li&gt;
            &lt;li&gt;Pipes to compose programs&lt;/li&gt;
            &lt;li&gt;A shell for scripting and interaction&lt;/li&gt;
            &lt;li&gt;The standard utilities (grep, sed, awk, cat, ls...)&lt;/li&gt;
            &lt;li&gt;Fork/exec process model&lt;/li&gt;
            &lt;li&gt;User/group permissions&lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;Linux is Unix-like. It&apos;s not Unix™, but it&apos;s Unix in the ways that matter.&lt;/p&gt;

        &lt;h2&gt;POSIX&lt;/h2&gt;

        &lt;p&gt;&lt;strong&gt;POSIX&lt;/strong&gt; (Portable Operating System Interface) is the IEEE standard that defines what &quot;Unix-like&quot; means technically. It specifies the system calls, shell behavior, and utilities that make Unix Unix. If your OS is POSIX-compliant, code written for Unix will run on it.&lt;/p&gt;

        &lt;h2&gt;How they relate&lt;/h2&gt;

        &lt;ul&gt;
            &lt;li&gt;Unix is the origin and the trademark&lt;/li&gt;
            &lt;li&gt;POSIX is the specification that captured what Unix &lt;em&gt;is&lt;/em&gt;&lt;/li&gt;
            &lt;li&gt;Unix-like means &quot;POSIX-compliant, spiritually Unix, but not branded&quot;&lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;The original Unix operating system died; the philosophy survived. See &lt;a href=&quot;/the-world-unix-made&quot;&gt;The world Unix made&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>We don&apos;t charge for privacy</title>
    <link href="https://mattobrien.org/we-dont-charge-for-privacy/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/we-dont-charge-for-privacy/</id>
    <published>2025-11-26T05:26:49.000Z</published>
    <updated>2025-11-26T05:26:49.000Z</updated>
    <summary>The difference between &apos;we promise not to look&apos; and &apos;we can&apos;t look&apos; shouldn&apos;t cost extra. Sovereignty isn&apos;t ours to sell.</summary>
    <content type="html">&lt;p&gt;There are two kinds of privacy in software: privacy by policy, and privacy by architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy by policy&lt;/strong&gt; means the service provider holds your data and the keys to look at it. They may promise not to look, but technically, they &lt;em&gt;can&lt;/em&gt;. You are trusting their organization, their employees, and their government not to compel them to look. Privacy is contingent on their restraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy by architecture&lt;/strong&gt; means the service provider cannot look at your data because they do not have the keys. The data may be encrypted in a way that they cannot decrypt, or hosted on your server that they cannot access. Even if they wanted to look, or were compelled to by a court, they couldn&apos;t.&lt;/p&gt;

&lt;p&gt;One requires trust. The other does not.&lt;/p&gt;

&lt;p&gt;Enterprises often demand architectural privacy. For consumers, architectural privacy is often treated as a luxury and locked behind expensive premium pricing tiers. (Eg via features like self-hosting, end-to-end encryption, and bring your own key.) Providers essentially say: &quot;Trusting us is free. Removing the need for trust costs extra.&quot;&lt;/p&gt;

&lt;p&gt;We reject this.&lt;/p&gt;

&lt;p&gt;We believe architectural privacy is the baseline. Privacy that depends on our restraint is not equivalent to privacy that&apos;s ontologically yours. You might choose our cloud for convenience, but your sovereignty isn&apos;t ours to sell.&lt;/p&gt;

&lt;p&gt;On Relay, self-hosting is free. The programmatic guarantee that we cannot access your data is available on every plan. We charge for the convenience and infrastructure we provide. We do not charge for the architecture that guarantees your privacy.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Unix was a child star with greedy parents</title>
    <link href="https://mattobrien.org/unix-was-a-child-star-with-greedy-parents/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/unix-was-a-child-star-with-greedy-parents/</id>
    <published>2025-11-22T06:18:43.000Z</published>
    <updated>2025-11-22T06:18:43.000Z</updated>
    <summary>An antitrust decree created an accidental open source phenomenon. When commercialization tore the platform apart, the philosophy escaped and conquered the world.</summary>
    <content type="html">&lt;p&gt;Ken Thompson and Dennis Ritchie created Unix at Bell Labs in 1969. They built it on a &lt;a href=&quot;/unix-philosophy&quot;&gt;foundation&lt;/a&gt; of radical simplicity: small tools that do one thing well, composability, and text as the universal interface.&lt;/p&gt;

        &lt;p&gt;It grew up in a unique period of benign neglect.&lt;/p&gt;

        &lt;p&gt;AT&amp;T was at the time a regulated monopoly. Under a 1956 antitrust consent decree, it was legally barred from engaging in any business other than common carrier communications. It could not sell software.&lt;/p&gt;

        &lt;p&gt;So it didn&apos;t. For 15 years, AT&amp;T distributed Unix to universities for the cost of the magnetic tape and shipping — roughly $150. They included the full source code, not because of ideology, but because they couldn&apos;t charge for support. (According to legend, Thompson, free of oversight, shipped the tapes himself with notes signed &quot;Love, Ken&quot;.)&lt;/p&gt;

        &lt;p&gt;This was decades before open source. Richard Stallman wouldn&apos;t launch the free software movement until 1983, and the term &apos;open source&apos; wouldn&apos;t be coined until 1998. In Unix&apos;s early era, the economics of high-value industrial assets with zero marginal cost were not at all understood. Unix got an accidental open source start because the owner was legally forbidden from capturing the value.&lt;/p&gt;

        &lt;p&gt;This anomaly allowed &lt;a href=&quot;/unix-scene&quot;&gt;the Unix scene&lt;/a&gt; to form. Universities modified the code, shared improvements, and trained a generation of students. By 1984, the &quot;child&quot; was a phenomenon, running on 45,000 systems.&lt;/p&gt;

        &lt;p&gt;Then the laws changed.&lt;/p&gt;

        &lt;p&gt;In 1984, the government broke up AT&amp;T and lifted the consent decree. The parents now saw their child as a goldmine.&lt;/p&gt;

        &lt;p&gt;AT&amp;T immediately commercialized Unix (System V). They tightened licensing and raised prices (universities: $800 for the first CPU; businesses: $43k for the first CPU).&lt;/p&gt;

        &lt;p&gt;Treating Unix as intellectual property to monetize rather than a platform to unify, AT&amp;T licensed the source code to rivals, who rushed to claim the estate. Sun Microsystems (Solaris), HP (HP-UX), IBM (AIX), and DEC (Ultrix) all built their own proprietary Unix versions.&lt;/p&gt;

        &lt;p&gt;The &quot;Unix Wars&quot; began.&lt;/p&gt;

        &lt;p&gt;These companies did not try to grow the commons; they tried to fence it. They created incompatible versions to lock customers into their hardware. Their actions were commercially rational, but they tore the kid apart.&lt;/p&gt;

        &lt;ul&gt;
            &lt;li&gt;Software written for HP-UX wouldn&apos;t run on AIX.&lt;/li&gt;
            &lt;li&gt;Solaris admins couldn&apos;t manage System V.&lt;/li&gt;
            &lt;li&gt;The shared community fractured into corporate camps.&lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;While the parents fought over custody, the audience left.&lt;/p&gt;

        &lt;p&gt;Microsoft offered what the Unix vendors refused to give: a single, unified standard. Windows NT (1993) swept the corporate market. By the late 90s, the proprietary Unix workstation was dying.&lt;/p&gt;

        &lt;p&gt;But the spirit survived.&lt;/p&gt;

        &lt;p&gt;In 1991, Linus Torvalds built Linux to replicate the environment he couldn&apos;t afford to buy. He wrote it from scratch — a &quot;clean room&quot; implementation with no AT&amp;T code — and offered it under the GPL license to codify the conditions of Unix&apos;s childhood: a system that was free, open, and owned by no one. He made it impossible for any vendor to fence the commons again.&lt;/p&gt;

        &lt;p&gt;The proprietary Unixes killed each other. The philosophy escaped and conquered the world. We now live in &lt;a href=&quot;/the-world-unix-made&quot;&gt;the world Unix made&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The world Unix made</title>
    <link href="https://mattobrien.org/the-world-unix-made/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/the-world-unix-made/</id>
    <published>2025-11-21T05:07:29.000Z</published>
    <updated>2025-11-21T05:07:29.000Z</updated>
    <summary>Unix is the nearest common ancestor of modern computing. Its bet: text as the universal interface.</summary>
    <content type="html">&lt;p&gt;Unix is the nearest common ancestor of modern computing.&lt;/p&gt;

        &lt;p&gt;
            The defining trait of the Unix paradigm is
            &lt;strong&gt;Text as the Universal Substrate&lt;/strong&gt;.
        &lt;/p&gt;

        &lt;p&gt;
            Before Unix, files were rigid containers. You specified their size
            and structure at creation time — and were locked in. Unix made files
            formatless. And it bet on text as the universal interface.
        &lt;/p&gt;

        &lt;p&gt;This decision created the lingua franca of computing.&lt;/p&gt;
        &lt;ul&gt;
            &lt;li&gt;Configuration is text.&lt;/li&gt;
            &lt;li&gt;Code is text.&lt;/li&gt;
            &lt;li&gt;The Web (HTML) is text.&lt;/li&gt;
            &lt;li&gt;Data interchange (JSON) is text.&lt;/li&gt;
            &lt;li&gt;APIs speak text.&lt;/li&gt;
            &lt;li&gt;Humans can read text.&lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;
            This paradigm conquered the world. The cloud runs on it. The mobile
            world (iOS and Android) runs on it. macOS runs on it. Even Windows
            adopted the
            &lt;a href=&quot;/unix-files-are-computings-shipping-containers&quot;
                &gt;Unix file paradigm&lt;/a
            &gt;
            (hierarchical byte streams) early in its history.
        &lt;/p&gt;

        &lt;p&gt;
            Unix is not the &lt;em&gt;genetic&lt;/em&gt; ancestor of all modern systems
            (though it is that for many), but it is the
            &lt;em&gt;spiritual&lt;/em&gt; ancestor: Unix built the civilization that all
            modern computing lives within.
        &lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Notion is simulated composability</title>
    <link href="https://mattobrien.org/notion-is-simulated-composability/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/notion-is-simulated-composability/</id>
    <published>2025-11-20T02:47:56.000Z</published>
    <updated>2025-11-20T02:47:56.000Z</updated>
    <summary>Notion offers simulated composability: building with pre-made parts in a bounded sandbox. Real building happens in the open world, where the standard is in the substrate.</summary>
    <content type="html">&lt;p&gt;Notion is a marvel. It feels powerful because it is. You can drag a database into a page, turn a list into a Kanban board, and link everything together. It feels like building.&lt;/p&gt;

&lt;p&gt;But it&apos;s a trap.&lt;/p&gt;

&lt;p&gt;Notion teaches you that building means assembling pre-made parts. It&apos;s simulated composability. Like Lego, the standard is in the &lt;strong&gt;joint&lt;/strong&gt;. The pieces snap together perfectly, but only where the designer put a stud. You can build a castle, but not a &lt;em&gt;real&lt;/em&gt; castle.&lt;/p&gt;

&lt;p&gt;The composition space is bounded by the designed interface. You are playing in their sandbox, with their physics.&lt;/p&gt;

&lt;p&gt;Real building happens in the open world.&lt;/p&gt;

&lt;p&gt;In the open world — text files, code, programmatic interfaces — the standard is in the &lt;strong&gt;substrate&lt;/strong&gt;. Because the substrate (text) is universal, you don&apos;t need pre-defined slots. The connection isn&apos;t a permission granted by the object; it&apos;s an operation you perform on the material. You can grep, pipe, script, and transform anything.&lt;/p&gt;

&lt;p&gt;Simulated composability is safer. It has training wheels. But training wheels don&apos;t teach you to balance—they teach you to lean. You learn to rely on the provided stability instead of creating your own.&lt;/p&gt;

&lt;p&gt;If you want to build something the designer didn&apos;t anticipate, you need a machine shop, not a Lego set.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Open-world vs closed-world composability</title>
    <link href="https://mattobrien.org/open-world-vs-closed-world-composability/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/open-world-vs-closed-world-composability/</id>
    <published>2025-11-19T07:40:00.000Z</published>
    <updated>2025-11-19T07:40:00.000Z</updated>
    <summary>Composability is a phenomenon with a scope - some systems constrain it to approved affordances, others leave it unbounded</summary>
    <content type="html">&lt;p&gt;Composability is a phenomenon with a scope.&lt;/p&gt;

&lt;p&gt;Some systems have constrained scope — closed-world composability, like a closed market:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notion blocks snap together within Notion&apos;s sandbox&lt;/li&gt;
&lt;li&gt;IKEA furniture pieces fit together in approved ways&lt;/li&gt;
&lt;li&gt;Lego bricks build anything, but only Lego-compatible structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bounded composition space. Constrained to approved affordances.&lt;/p&gt;

&lt;p&gt;Other systems have large or unbounded scope — open-world composability, like an open market:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Text files: any tool can read and write them, no vendor permission needed&lt;/li&gt;
&lt;li&gt;Unix pipes: combine any programs that speak text&lt;/li&gt;
&lt;li&gt;Lumber: build anything physics allows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unbounded composition space. Any tool can participate without permission.&lt;/p&gt;

&lt;p&gt;The difference is who controls the composition space.&lt;/p&gt;

&lt;p&gt;Closed-world systems give you pieces and rules for combining them. You can build impressive things, but only within the boundaries the system defines.&lt;/p&gt;

&lt;p&gt;Open-world systems give you substrate that any tool can work with. The composition space isn&apos;t bounded by what one vendor imagined. It&apos;s bounded by what&apos;s logically possible and what the actors in the ecosystem create.&lt;/p&gt;

&lt;p&gt;Open-world systems have greater potential for exponential growth because the number of possible combinations grows with every new tool that can participate, not just with the combinations the original designer anticipated. More participants → more combinations → more network effects → combinatorial explosion.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Disruptive vs sustaining innovations</title>
    <link href="https://mattobrien.org/disruptive-vs-sustaining/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/disruptive-vs-sustaining/</id>
    <published>2025-11-18T08:57:49.000Z</published>
    <updated>2025-11-18T08:57:49.000Z</updated>
    <summary>Sustaining innovations serve existing customers better. Disruptive innovations are those the established firm is rational not to pursue, which then gain significant traction.</summary>
    <content type="html">&lt;p&gt;Disruption is defined in retrospect: disruptive innovations are those that disrupted.&lt;/p&gt;

&lt;p&gt;Clayton Christensen&apos;s famous line of inquiry asked why well-run companies get disrupted by new technologies. By studying cases of disruption, he hoped to identify advance signals that managers in established companies could use to avoid disruption, or that startups could use to foster it.&lt;/p&gt;

&lt;p&gt;Between the initial article and his death in 2020, Christensen refined his model many times. The core characteristics he identified were:&lt;/p&gt;

&lt;h2&gt;Sustaining innovations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Common - the vast majority&lt;/li&gt;
&lt;li&gt;Improve existing products on dimensions customers already value&lt;/li&gt;
&lt;li&gt;Can be radical or incremental - doesn&apos;t matter&lt;/li&gt;
&lt;li&gt;Established competitors almost always win&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Disruptive innovations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Rare&lt;/li&gt;
&lt;li&gt;Start worse on traditional performance metrics&lt;/li&gt;
&lt;li&gt;Cheaper, simpler, smaller, more convenient&lt;/li&gt;
&lt;li&gt;Target nonconsumers or overserved low-end customers&lt;/li&gt;
&lt;li&gt;Different value proposition&lt;/li&gt;
&lt;li&gt;Established firms rationally ignore them&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Limitations and usefulness&lt;/h2&gt;

&lt;p&gt;Christensen wanted these signals to predict disruption in advance, but it hasn&apos;t really worked. Perhaps that&apos;s because patterns, once identified, are learned by the next generation of business managers, who avoid getting disrupted for the old reasons and instead get disrupted for new ones.&lt;/p&gt;

&lt;p&gt;If that&apos;s the case, then it should still benefit the manager to gain familiarity with all the old patterns. Practice in this line of thinking may help one to avoid or foster disruption.&lt;/p&gt;

&lt;h2&gt;From Clayton Christensen 1997 - The Innovator&apos;s Dilemma&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;technology, as used in this book, means the processes by which an organization transforms labor, capital, materials, and information into products and services of greater value&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
&lt;p&gt;Innovation refers to a change in one of these technologies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
&lt;p&gt;there is a strategically important distinction between what I call sustaining technologies and those that are disruptive. These concepts are very different from the incremental-versus-radical distinction that has characterized many studies of this problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sustaining versus Disruptive Technologies&lt;/strong&gt;&lt;br&gt;
Most new technologies foster improved product performance. I call these sustaining technologies. Some sustaining technologies can be discontinuous or radical in character, while others are of an incremental nature. What all sustaining technologies have in common is that they improve the performance of established products, along the dimensions of performance that mainstream customers in major markets have historically valued. Most technological advances in a given industry are sustaining in character. An important finding revealed in this book is that rarely have even the most radically difficult sustaining technologies precipitated the failure of leading firms.&lt;/p&gt;

&lt;p&gt;Occasionally, however, disruptive technologies emerge: innovations that result in worse product performance, at least in the near-term. Ironically, in each of the instances studied in this book, it was disruptive technology that precipitated the leading firms&apos; failure.&lt;/p&gt;

&lt;p&gt;Disruptive technologies bring to a market a very different value proposition than had been available previously. Generally, disruptive technologies underperform established products in mainstream markets. But they have other features that a few fringe (and generally new) customers value. Products based on disruptive technologies are typically cheaper, simpler, smaller, and, frequently, more convenient to use.&lt;/p&gt;
&lt;/blockquote&gt;</content>
  </entry>
  <entry>
    <title>Programmatic interfaces vs graphical user interfaces (GUIs)</title>
    <link href="https://mattobrien.org/programmatic-vs-gui/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/programmatic-vs-gui/</id>
    <published>2025-11-15T02:30:00.000Z</published>
    <updated>2025-11-15T02:30:00.000Z</updated>
    <summary>Two ways to interface with software - programmatic (for technical humans and machines) vs graphical (for humans)</summary>
    <content type="html">&lt;p&gt;Two ways to interface with software:&lt;/p&gt;

&lt;h2&gt;Programmatic interfaces&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Interface through code, commands, text&lt;/li&gt;
&lt;li&gt;Examples: command-line (CLI), APIs, stdin/stdout, &lt;code&gt;SELECT * FROM users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Requires BYO mental model: interface doesn&apos;t educate, expects you to know what&apos;s possible&lt;/li&gt;
&lt;li&gt;Used by: humans (developers) AND machines (scripts, automation, AI)&lt;/li&gt;
&lt;li&gt;Not accessible to non-technical people&lt;/li&gt;
&lt;li&gt;Might have &lt;code&gt;help&lt;/code&gt; or &lt;code&gt;man&lt;/code&gt; pages, but minimal — the interface executes, doesn&apos;t teach&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Graphical User Interfaces (GUIs)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Interface through visual elements — buttons, windows, menus, forms, frames&lt;/li&gt;
&lt;li&gt;Examples: Google Docs, MarioKart, this website&lt;/li&gt;
&lt;li&gt;Present a model: visual menu of affordances, skeuomorphic metaphors (desktop, folders, trash)&lt;/li&gt;
&lt;li&gt;Used by: humans (point, click, drag, type)&lt;/li&gt;
&lt;li&gt;Machines CAN use them (screen scraping, AI computer use) but it&apos;s clunky and far behind agentic programmatic computer use&lt;/li&gt;
&lt;li&gt;Self-educating: can show up knowing little, learn by exploring&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>AI is a universal GUI to programmatic tools</title>
    <link href="https://mattobrien.org/ai-universal-adapter/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/ai-universal-adapter/</id>
    <published>2025-11-14T05:52:40.000Z</published>
    <updated>2025-11-14T05:52:40.000Z</updated>
    <summary>Non-developers can chat with AI, and AI can use any programmatic tool, so non-developers can access the developer stack</summary>
    <content type="html">&lt;p&gt;Though programmatic interfaces are generally &lt;a href=&quot;/engineers-prefer-programmatic&quot;&gt;more powerful&lt;/a&gt; than GUIs, they also have a steeper learning curve. They do not present a welcoming visual metaphor and menu of affordances. The user has to come pre-supplied with a mental model and sense of what&apos;s possible. That&apos;s why GUIs have been so popular.&lt;/p&gt;

&lt;p&gt;Language models change the game. Everyone knows how to chat, and language models know (or will soon know) how to use every programmatic tool.&lt;/p&gt;

&lt;h2&gt;AI speaks two languages: natural language to you, technical syntax to tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You describe what you want in conversation&lt;/li&gt;
&lt;li&gt;AI translates that into commands: grep, git, API calls, etc&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;AI is a universal GUI to programmatic tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Anyone can interface with AI through conversation&lt;/li&gt;
&lt;li&gt;AI can interface with any programmatic tool&lt;/li&gt;
&lt;li&gt;This means anyone can now access any programmatic tool&lt;/li&gt;
&lt;li&gt;The entire developer stack - git, grep, APIs, command-line tools, file manipulation - opens up to non-developers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;You can learn and build at the same time&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;AI is also a capable tutor&lt;/li&gt;
&lt;li&gt;Ask as you go, at whatever level you&apos;re at. &quot;How do I build a website?&quot; &quot;What&apos;s a commit?&quot; &quot;How do I cd into a folder?&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;The barrier is ingenuity, not syntax fluency&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Old requirement: Know the commands before you can use the tools&lt;/li&gt;
&lt;li&gt;New requirement: Conversational fluency + willingness to explore unfamiliar territory&lt;/li&gt;
&lt;li&gt;If you can describe what you want and ask &quot;what does that mean?&quot; when you don&apos;t understand - you can do this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;More people can access the programmatic stack&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Not everyone - still requires curiosity, confidence to try new tools, comfort with the unknown&lt;/li&gt;
&lt;li&gt;But anyone with those traits can now access the powerful layer that was previously developer-only&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Software engineers prefer programmatic interfaces</title>
    <link href="https://mattobrien.org/engineers-prefer-programmatic/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/engineers-prefer-programmatic/</id>
    <published>2025-11-13T07:00:00.000Z</published>
    <updated>2025-11-13T07:00:00.000Z</updated>
    <summary>Software engineers often prefer programmatic interfaces because they offer greater control</summary>
    <content type="html">&lt;p&gt;Software engineers often prefer programmatic interfaces because they offer greater control.&lt;/p&gt;

&lt;h2&gt;Software that has no graphical user interface is sometimes called &quot;headless&quot;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The &quot;head&quot; is the GUI - buttons, windows, forms, visual interface&lt;/li&gt;
&lt;li&gt;Headless tools don&apos;t have that layer. Their interface is text.&lt;/li&gt;
&lt;li&gt;Examples: Terminal commands (stdin/stdout, text streams), APIs (HTTP requests/responses, JSON, text protocols)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Developers are fluent in programmatic environments&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Git for version control, grep for searching, Docker for containers, APIs and calling them — all text-based&lt;/li&gt;
&lt;li&gt;IDEs are GUI tools, but designed for headless-native developers — they integrate terminals, expose git commands, and keep the text layer accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Programmatic interfaces were first&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Before GUIs existed (60s and 70s), this was the only option. Not enough computing power for graphical interfaces even if they&apos;d been invented.&lt;/li&gt;
&lt;li&gt;GUIs came later as a translation layer for non-technical users&lt;/li&gt;
&lt;li&gt;Developers never switched because the direct interface remained more powerful&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Programmatic is powerful&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Headless is where full capabilities are accessible&lt;/li&gt;
&lt;li&gt;When you click a button in a GUI, it&apos;s always calling some underlying programmatic interface — a command-line tool, a library API, a system call, etc. The headless layer is always underneath.&lt;/li&gt;
&lt;li&gt;To the non-developer the headless tool seems barren, limited, sparse.&lt;/li&gt;
&lt;li&gt;To the developer the GUI feels like a middleman. Sometimes helpful, often not.&lt;/li&gt;
&lt;li&gt;Working headless means you&apos;re operating at a more foundational layer. Not the lowest layer (machine code), but the layer that GUIs are built on top of.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Programmatic interfaces require BYO mental model&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A GUI is a visual menu of affordances. It presents a model, often using skeuomorphic metaphors. The user can show up knowing little and learn on the fly.&lt;/li&gt;
&lt;li&gt;Headless tools require that the user show up with a mental model not supplied by the interface. You need to already have a good sense of what&apos;s possible.&lt;/li&gt;
&lt;li&gt;That learning curve limited adoption by non-developers&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>What is a design provocation?</title>
    <link href="https://mattobrien.org/design-provocation/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/design-provocation/</id>
    <published>2025-11-12T05:49:31.000Z</published>
    <updated>2025-11-12T05:49:31.000Z</updated>
    <summary>A question that pushes you to explore new neighborhoods of solution space</summary>
    <content type="html">&lt;p&gt;A design provocation is a prompt that stimulates you to explore novel areas of solution space. A good one will lead to solutions better than those previously known.&lt;/p&gt;

&lt;p&gt;A connotation of &apos;provoke&apos; is that when someone is provoked they might go too far. A design provocation might make you go too far.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Provocation: You have to do the whole thing in 1 month.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: You can&apos;t use words at all.&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result of going too far is that you have expanded the Overton window of the design space you&apos;re exploring for solutions. If it worked, the best solution will now be within the boundaries of the space you are exploring.&lt;/p&gt;

&lt;p&gt;Another connotation of &apos;provoke&apos; is emotional arousal. A good design provocation wakes something up in you and brings more of your emotional resources to bear on the problem.&lt;/p&gt;

&lt;p&gt;Provocations can be framed as a question (&quot;What if we had to do it in 1 month?&quot;) or as an imperative (&quot;Provocation: We have to do it in 1 month&quot;). The imperative form is often more provocative.&lt;/p&gt;

&lt;h2&gt;What makes a good one&lt;/h2&gt;

&lt;p&gt;They&apos;re good when, in retrospect, they helped us find a better solution. Often this better solution was an interpolation between the wild ideas generated by the exercise and the orthodox ideas we started with. The design provocation helped us get there.&lt;/p&gt;

&lt;p&gt;They&apos;re not so good when they had us exploring areas that weren&apos;t fruitful. A design provocation could be, &quot;It has to be 100% purple.&quot; For most projects, that&apos;s not going to turn out to be useful.&lt;/p&gt;

&lt;p&gt;It&apos;s not fully decidable in advance whether a provocation is going be good. You have to pick one based on experience and intuition. You&apos;ll find out in the end whether it worked.&lt;/p&gt;

&lt;h2&gt;How to generate them&lt;/h2&gt;

&lt;p&gt;Some approaches:&lt;/p&gt;

&lt;p&gt;1. Radical amplification - Notice something desirable (characteristic X), propose it radically&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Provocation: It has to take zero clicks.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: We won&apos;t listen to that guy at all.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: The goal is that kids prefer it over going on vacation.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: That number has to be 100x bigger.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: We&apos;re building the best one in the world.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: We have to do it in 1 month.&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2. Inversion - What would destroy the product or tank the business?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Provocation: Our goal is to destroy user trust.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: We want this to be super aggravating to use.&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3. Lateral thinking - Reframe the problem entirely&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;What if we had to solve the problem in a completely different way?&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: The problem isn&apos;t a problem at all.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: The real problem is our way of conceptualizing this.&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4. Personas - Channel someone else&apos;s perspective&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Provocation: We approach this like Steve Jobs would.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: We do what Jesus would do.&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5. Feelings first - Start with the emotional response you want&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Provocation: The goal is that I feel joy when I use this.&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;6. Crazy ideas - Remove all constraints&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;What if we solved this in the most batshit way possible?&quot;&lt;/li&gt;
&lt;li&gt;&quot;What if we had a magic wand and could [xyz, eg, have unlimited engineering resources]?&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;7. Artifacts - Reference existing solutions&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Provocation: It has to be like [artifact].&quot;&lt;/li&gt;
&lt;li&gt;&quot;Provocation: We employ the same method the [artifact] creators used.&quot;&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Unix files are computing&apos;s shipping containers</title>
    <link href="https://mattobrien.org/unix-files-are-computings-shipping-containers/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/unix-files-are-computings-shipping-containers/</id>
    <published>2025-11-11T06:00:00.000Z</published>
    <updated>2025-11-11T06:00:00.000Z</updated>
    <summary>The Unix file and the intermodal container both represent format-agnostic, standardized interfaces that enabled revolutionary levels of interoperability.</summary>
    <content type="html">&lt;p&gt;The Unix file and the intermodal container both represent format-agnostic, standardized interfaces that enabled revolutionary levels of interoperability.&lt;/p&gt;

&lt;p&gt;Malcolm McLean&apos;s shipping containers standardized physical cargo handling by making the contents irrelevant to the transportation system—a 20-foot or 40-foot steel box with standard corner fittings could contain anything and move seamlessly across ships, trains, and trucks.&lt;/p&gt;

&lt;p&gt;In 1969 Ken Thompson implemented Unix files as formatless byte streams — a universal envelope that didn&apos;t require declaring type or size at file creation. Unix files standardized data handling by making the contents irrelevant to the system — files moved seamlessly through pipes, filters, and programs. See AT&amp;T engineer Catherine Ann Brooks describe the system in 1982: &lt;a href=&quot;https://youtu.be/XvDZLjaCJuw?si=CesJuFNqUYATWSyr&amp;t=667&quot;&gt;https://youtu.be/XvDZLjaCJuw?si=CesJuFNqUYATWSyr&amp;t=667&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both innovations solved similar problems: the proliferation of incompatible formats that prevented efficient composition and transfer. Before shipping containers, different cargo types required specialized handling and couldn&apos;t easily transfer between transportation modes. Before Unix&apos;s byte stream abstraction, different file formats required specialized programs and couldn&apos;t easily compose through pipelines. Both solutions involved imposing a universal, neutral interface that pushed format-specific knowledge to the edges of the system.&lt;/p&gt;

&lt;p&gt;Both innovations achieved transformative results, and both required ecosystem-wide adoption to realize their full value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Footnote:&lt;/strong&gt; Docker Inc. analogizes its containers to the intermodal container. The metaphor is apt, but arguably, the Unix file was more transformative.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The Unix scene</title>
    <link href="https://mattobrien.org/unix-scene/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/unix-scene/</id>
    <published>2025-11-08T04:15:00.000Z</published>
    <updated>2025-11-08T04:15:00.000Z</updated>
    <summary>The Unix scene peaked at 40,000 in 1985-88, dissolved by 1995. Today 6.5B people use Unix-descended systems.</summary>
    <content type="html">&lt;p&gt;Unix was a &lt;a href=&quot;/scene&quot;&gt;scene&lt;/a&gt;.&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Unix scene members&lt;/th&gt;
&lt;th&gt;Unix-influenced users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1966&lt;/td&gt;
&lt;td&gt;Thompson joins Bell Labs. Unix doesn&apos;t exist yet.&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1965-1969&lt;/td&gt;
&lt;td&gt;Thompson and Ritchie work on Multics project at Bell Labs (joint project with MIT and GE).&lt;/td&gt;
&lt;td&gt;0 (not Unix yet)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1969&lt;/td&gt;
&lt;td&gt;Bell Labs withdraws from Multics project.&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1969 (summer)&lt;/td&gt;
&lt;td&gt;Thompson writes Space Travel game on PDP-7, designs simple file system.&lt;/td&gt;
&lt;td&gt;~2&lt;/td&gt;
&lt;td&gt;~2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1970&lt;/td&gt;
&lt;td&gt;First running Unix system (Thompson + Ritchie). Name changes from &quot;Unics&quot; to &quot;Unix&quot;.&lt;/td&gt;
&lt;td&gt;~3&lt;/td&gt;
&lt;td&gt;~3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1971&lt;/td&gt;
&lt;td&gt;First Edition Unix released internally at Bell Labs. Small group using it: Thompson, Ritchie, McIlroy, few others in the research group.&lt;/td&gt;
&lt;td&gt;~12&lt;/td&gt;
&lt;td&gt;~15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1972&lt;/td&gt;
&lt;td&gt;Ritchie begins developing C language. Unix group at Bell Labs growing - more researchers adopting it.&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;~75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1973 (summer)&lt;/td&gt;
&lt;td&gt;Unix rewritten in C (makes it portable). McIlroy suggests pipes, Thompson implements in one night. Philosophy crystallizes.&lt;/td&gt;
&lt;td&gt;~60&lt;/td&gt;
&lt;td&gt;~150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1973&lt;/td&gt;
&lt;td&gt;Fourth Edition Unix. Being presented at conferences. Papers published. Philosophy getting articulated.&lt;/td&gt;
&lt;td&gt;~100&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1974-1975&lt;/td&gt;
&lt;td&gt;AT&amp;T begins distributing to universities (nearly free, with source code). Berkeley gets it.&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;td&gt;~1,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1975&lt;/td&gt;
&lt;td&gt;First USENIX conference (40 attendees, 20 institutions). Community infrastructure begins forming.&lt;/td&gt;
&lt;td&gt;~400&lt;/td&gt;
&lt;td&gt;~2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1978&lt;/td&gt;
&lt;td&gt;First BSD (Berkeley Software Distribution) release (30 copies distributed). Growing university adoption.&lt;/td&gt;
&lt;td&gt;~900&lt;/td&gt;
&lt;td&gt;~4,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1979&lt;/td&gt;
&lt;td&gt;Seventh Edition Unix (V7). Widely considered the most important Unix.&lt;/td&gt;
&lt;td&gt;~1,500&lt;/td&gt;
&lt;td&gt;~7,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1980&lt;/td&gt;
&lt;td&gt;Usenet begins. Community infrastructure emerges - people can share code and discussions asynchronously.&lt;/td&gt;
&lt;td&gt;~2,000&lt;/td&gt;
&lt;td&gt;~12,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1983&lt;/td&gt;
&lt;td&gt;TCP/IP added to BSD (4.2BSD). BSD development accelerates. Unix becomes the internet OS.&lt;/td&gt;
&lt;td&gt;~7,500&lt;/td&gt;
&lt;td&gt;~75,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1983&lt;/td&gt;
&lt;td&gt;AT&amp;T System V Release 1. AT&amp;T commercializes Unix. Scene begins fragmenting between commercial Unix and BSD.&lt;/td&gt;
&lt;td&gt;~10,000&lt;/td&gt;
&lt;td&gt;~150,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1984&lt;/td&gt;
&lt;td&gt;45,000 Unix systems installed, 940 Usenet hosts. Scene thriving but fragmenting.&lt;/td&gt;
&lt;td&gt;~15,000&lt;/td&gt;
&lt;td&gt;~300,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1985&lt;/td&gt;
&lt;td&gt;Microsoft Windows 1.0 released. Not Unix-based (runs on top of DOS).&lt;/td&gt;
&lt;td&gt;~40,000&lt;/td&gt;
&lt;td&gt;~2M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1985-1988&lt;/td&gt;
&lt;td&gt;PEAK. USENIX conferences draw ~3K attendees. CS degrees peak at 42K/year. Scene at maximum size but losing coherence.&lt;/td&gt;
&lt;td&gt;~40,000&lt;/td&gt;
&lt;td&gt;~2M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1988&lt;/td&gt;
&lt;td&gt;Unix Wars begin. Rival consortia form (OSF vs Unix International). Scene fragmenting into incompatible camps.&lt;/td&gt;
&lt;td&gt;Fragmenting&lt;/td&gt;
&lt;td&gt;~4M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1989&lt;/td&gt;
&lt;td&gt;NeXTSTEP released by Steve Jobs&apos; NeXT Computer. Unix-based (BSD foundation).&lt;/td&gt;
&lt;td&gt;~45,000 (fragmenting)&lt;/td&gt;
&lt;td&gt;~4M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1991&lt;/td&gt;
&lt;td&gt;Linux kernel first released by Linus Torvalds. Unix-like but not Unix. Free and open source. NEW scene begins, separate from Unix.&lt;/td&gt;
&lt;td&gt;Unix: ~20,000, Linux/BSD: ~300&lt;/td&gt;
&lt;td&gt;~15M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1993&lt;/td&gt;
&lt;td&gt;Windows NT released. Not Unix. Begins Microsoft&apos;s dominance in corporate computing. Commercial Unix systems (Solaris, HP-UX, AIX) start declining.&lt;/td&gt;
&lt;td&gt;Unix: ~15,000, Linux/BSD: ~3,000&lt;/td&gt;
&lt;td&gt;~30M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1995&lt;/td&gt;
&lt;td&gt;Windows 95 released. Huge commercial success. Not Unix. Unix becomes increasingly niche in desktop/consumer markets. Original Unix scene dissolves - BSD CSRG closes after 20 years.&lt;/td&gt;
&lt;td&gt;Unix scene ended, Linux/BSD: ~30,000&lt;/td&gt;
&lt;td&gt;~40M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1997&lt;/td&gt;
&lt;td&gt;Apple acquires NeXT. NeXTSTEP becomes foundation for future macOS.&lt;/td&gt;
&lt;td&gt;Unix: ~7,500, Linux/BSD: ~75,000&lt;/td&gt;
&lt;td&gt;~65M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;Mac OS X released. Unix-based (BSD + NeXTSTEP). First mainstream consumer Unix.&lt;/td&gt;
&lt;td&gt;Unix: ~7,500, Linux/BSD: ~500,000&lt;/td&gt;
&lt;td&gt;~125M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2007&lt;/td&gt;
&lt;td&gt;iPhone released. Runs iOS, which is based on macOS (thus Unix-based). Unix in billions of pockets, users unaware.&lt;/td&gt;
&lt;td&gt;Unix: ~10,000, Linux/BSD: ~2M&lt;/td&gt;
&lt;td&gt;~300M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;Android released. Based on Linux kernel.&lt;/td&gt;
&lt;td&gt;Unix: ~10,000, Linux/BSD: ~3M&lt;/td&gt;
&lt;td&gt;~750M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2014-2015&lt;/td&gt;
&lt;td&gt;Unix DNA reaches billions of users via smartphones. Most people use Unix-descended systems daily without knowing.&lt;/td&gt;
&lt;td&gt;Unix: ~10,000, Linux/BSD: ~8M&lt;/td&gt;
&lt;td&gt;~2.5B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;Today. Commercial Unix systems (Solaris, HP-UX, AIX) nearly extinct. Replaced by Linux in enterprise, macOS in consumer/creative markets, Windows in corporate. Original Unix scene dissolved - descendants (Linux, macOS) dominate computing but are too large/diffuse to be a &quot;scene&quot;.&lt;/td&gt;
&lt;td&gt;Unix: ~10,000, Linux/BSD: ~10M&lt;/td&gt;
&lt;td&gt;~6.5B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The scene dissolved, but &lt;a href=&quot;/unix-philosophy&quot;&gt;The Unix philosophy&lt;/a&gt; won — it&apos;s everywhere. Most servers run Linux. Macs and iPhones run Unix. Android runs Linux. Windows is the outlier. Today most of humanity uses Unix-descended systems.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Scene</title>
    <link href="https://mattobrien.org/scene/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/scene/</id>
    <published>2025-11-08T00:50:00.000Z</published>
    <updated>2025-11-08T00:50:00.000Z</updated>
    <summary>An active community around shared interest with informal networks, recognizable values, and generative culture.</summary>
    <content type="html">&lt;p&gt;A scene is a social-cultural formation around some shared interest/practice/aesthetic.&lt;/p&gt;

&lt;h2&gt;Characteristics of a &quot;scene&quot;:&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Critical mass but not dominance - More than a few people, less than mainstream. &quot;There&apos;s a real scene for X&quot; means it&apos;s active and vibrant, not that everyone&apos;s doing it.&lt;/li&gt;

&lt;li&gt;Recognizability - People in the scene recognize each other. You can tell who&apos;s &quot;in&quot; the scene. Not formalized membership, but you know it when you see it.&lt;/li&gt;

&lt;li&gt;Shared values/aesthetics - Not just doing the same thing, but having similar sensibilities about HOW and WHY to do it.&lt;/li&gt;

&lt;li&gt;Informal networks - Not an organization, no official membership. People know people, information flows, things get shared.&lt;/li&gt;

&lt;li&gt;Cultural artifacts - The scene produces things: music, zines, code, writing, events. The artifacts carry the scene&apos;s DNA.&lt;/li&gt;

&lt;li&gt;Generativity - A good scene generates new work, new people, new variations. It&apos;s alive, not static.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Examples:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Punk rock scene (70s-80s): shared venues, DIY ethic, recognizable aesthetic&lt;/li&gt;
&lt;li&gt;Early web scene (90s): personal sites, webrings, shared ethos about open web&lt;/li&gt;
&lt;li&gt;Indie game dev scene (now): itch.io, GDC, shared values about creative independence&lt;/li&gt;
&lt;li&gt;Local-first scene (now): Ink &amp; Switch, CRDTs, Obsidian, file-over-app, sovereignty values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &quot;Scene&quot; was also a specific music subculture in the mid-2000s (MySpace era, post-hardcore aesthetic). The term came from general music slang (&quot;being on the scene&quot;) but became the proper name for that specific group. I&apos;m using scene in the general sense.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The Unix philosophy</title>
    <link href="https://mattobrien.org/unix-philosophy/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/unix-philosophy/</id>
    <published>2025-11-07T06:05:52.000Z</published>
    <updated>2025-11-07T06:05:52.000Z</updated>
    <summary>Small programs that do one thing well, composability, and text streams as universal interface</summary>
    <content type="html">&lt;p&gt;Unix was an operating system created at Bell Labs in the late 60s and early 70s. Before Unix, creator Ken Thompson worked on Multics — a system designed to do everything. The ultimate monolith. Thomson found it hard to learn and not fun to use. He hated it, and Bell Labs eventually abandoned it. Watch him talk about it in a recent oral history: &lt;a href=&quot;https://youtu.be/OmVHkL0IWk4?si=nQAcSy6LE1VY5iiu&amp;t=4687&quot;&gt;https://youtu.be/OmVHkL0IWk4?si=nQAcSy6LE1VY5iiu&amp;t=4687&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Unix philosophy was a response.&lt;sup&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;In its most famous statement: &quot;Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.&quot;&lt;sup&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;1. Small programs that do one thing well&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Not one giant program with all features&lt;/li&gt;
&lt;li&gt;Many focused tools instead&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep&lt;/code&gt; only searches text. &lt;code&gt;sort&lt;/code&gt; only sorts lines. &lt;code&gt;cat&lt;/code&gt; only concatenates files.&lt;/li&gt;
&lt;li&gt;Each tool is simple, understandable, good at its job&lt;/li&gt;
&lt;li&gt;You can learn each tool independently&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;2. Programs work together (composability)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tools compose via pipes and text streams&lt;/li&gt;
&lt;li&gt;The output of one becomes the input to another&lt;/li&gt;
&lt;li&gt;Complex operations build up from simple pieces&lt;/li&gt;
&lt;li&gt;Clean, simple interfaces between components (text in, text out)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;3. Text streams as universal interface&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Everything speaks the same language (text)&lt;/li&gt;
&lt;li&gt;Any tool can connect to any other tool&lt;/li&gt;
&lt;li&gt;This is what makes composition work&lt;/li&gt;
&lt;li&gt;&quot;&lt;a href=&quot;/unix-files-are-computings-shipping-containers&quot;&gt;Everything is a file&lt;/a&gt;&quot; — even devices and processes expose text interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;p&gt;&lt;strong&gt;Footnotes:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; The Unix philosophy emerged in practice before it was formalized in linguistic maxims. An important early articulation came from Doug McIlroy, inventor of Unix pipes, in the Bell System Technical Journal in 1978 — nine years after Unix began. He &lt;a href=&quot;https://danluu.com/mcilroy-unix/&quot;&gt;wrote&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A number of maxims have gained currency among the builders and users of the UNIX system to explain and promote its characteristic style:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new &quot;features.&quot;&lt;/li&gt;
&lt;li&gt;Expect the output of every program to become the input to another, as yet unknown, program. Don&apos;t clutter output with extraneous information. Avoid stringently columnar or binary input formats. Don&apos;t insist on interactive input.&lt;/li&gt;
&lt;li&gt;Design and build software, even operating systems, to be tried early, ideally within weeks. Don&apos;t hesitate to throw away the clumsy parts and rebuild them.&lt;/li&gt;
&lt;li&gt;Use tools in preference to unskilled help to lighten a programming task, even if you have to detour to build the tools and expect to throw some of them out after you&apos;ve finished using them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Illustrations of these maxims are legion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Surprising to outsiders is the fact that UNIX compilers produce no listings: printing can be done better and more flexibly by a separate program.&lt;/li&gt;
&lt;li&gt;Unexpected uses of files abound: programs may be compiled to be run and also typeset to be published in a book from the same text without human intervention; text intended for publication serves as grist for statistical studies of English to help in data compression or cryptography; mailing lists turn into maps. The prevalence of free-format text, even in &quot;data&quot; files, makes the text-processing utilities useful for many strictly data processing functions such as shuffling fields, counting, or collating.&lt;/li&gt;
&lt;li&gt;The UNIX system and the C language themselves evolved by deliberate steps from early working models that had at most a few man-months invested in them. Both have been fully recoded several times by the same people who designed them, with as much mechanical aid as possible.&lt;/li&gt;
&lt;li&gt;The use of tools instead of labor is nicely illustrated by typesetting. When a paper needs a new layout for some reason, the typographic conventions for paragraphs, subheadings, etc. are entered in one place, then the paper is run off in the new shape without retyping a single word.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;sup&gt;2&lt;/sup&gt; Peter H. Salus, &lt;em&gt;A Quarter-Century of Unix&lt;/em&gt; (1994).&lt;/p&gt;
&lt;/div&gt;</content>
  </entry>
  <entry>
    <title>Monolithic vs composable</title>
    <link href="https://mattobrien.org/monolithic-vs-composable/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/monolithic-vs-composable/</id>
    <published>2025-11-06T04:14:18.000Z</published>
    <updated>2025-11-06T04:14:18.000Z</updated>
    <summary>Two approaches to building software with large capabilities: one giant system that does everything, or many small tools that work together.</summary>
    <content type="html">&lt;p&gt;Two different philosophies for building systems with large capabilities.&lt;/p&gt;

&lt;h2&gt;Monolithic approach&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Build one giant system that does everything&lt;/li&gt;
&lt;li&gt;All functionality integrated inside the system&lt;/li&gt;
&lt;li&gt;Your data lives inside it&lt;/li&gt;
&lt;li&gt;Proprietary formats internal to the system&lt;/li&gt;
&lt;li&gt;No interchange format (or very limited - maybe an API, maybe export)&lt;/li&gt;
&lt;li&gt;User works entirely within the monolith&lt;/li&gt;
&lt;li&gt;Examples: Notion (tries to be notes + database + wiki + project management), Salesforce (giant integrated CRM), Adobe Creative Cloud (integrated suite with proprietary formats), 60s mainframe systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Composable approach&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Build many small tools that each do one thing well&lt;/li&gt;
&lt;li&gt;Tools work together via standard interchange formats&lt;/li&gt;
&lt;li&gt;Your data lives in files that any tool can access&lt;/li&gt;
&lt;li&gt;Open formats (text, markdown, etc)&lt;/li&gt;
&lt;li&gt;Files/text streams are the universal interchange format&lt;/li&gt;
&lt;li&gt;User controls how tools compose&lt;/li&gt;
&lt;li&gt;Examples: Unix tools (grep, sed, awk, git), markdown files (write in any editor), Git, Obsidian (works with files on your filesystem)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;The tradeoffs&lt;/h2&gt;

&lt;p&gt;Monolithic systems can be easier initially — everything is integrated, works together out of the box, consistent interface.&lt;/p&gt;

&lt;p&gt;Composable systems require more initial setup but give you more control — you choose your tools and customize them, create your own workflows, own your data in open formats. Over time, arguably, the ceiling is higher.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Local is faster</title>
    <link href="https://mattobrien.org/local-is-faster/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/local-is-faster/</id>
    <published>2025-11-05T06:21:00.000Z</published>
    <updated>2025-11-05T06:21:00.000Z</updated>
    <summary>Every task has a speed requirement and a latency. At some threshold, the time it takes exceeds the time available, and the task becomes impossible.</summary>
    <content type="html">&lt;p&gt;Sometimes quantity has a quality all its own.&lt;/p&gt;

&lt;p&gt;Every task has a speed requirement and a latency. At some threshold, the time it takes to complete the task exceeds the time available, and the task becomes impossible.&lt;/p&gt;

&lt;p&gt;For example, how much typing latency do you think you&apos;d tolerate? A fifth of a second (200 ms)? A fiftieth (20 ms)? Make your guess and then find out: &lt;a href=&quot;https://aresluna.org/keyboard-secrets/typing-delay/&quot;&gt;https://aresluna.org/keyboard-secrets/typing-delay/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Local operations tend to be faster than those requiring communication with a remote resource. Here&apos;s Peter Norvig&apos;s famous list of times every beginning programmer should know (circa 2012):&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;execute typical instruction&lt;/td&gt;
&lt;td&gt;1 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch from L1 cache memory&lt;/td&gt;
&lt;td&gt;0.5 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;branch misprediction&lt;/td&gt;
&lt;td&gt;5 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch from L2 cache memory&lt;/td&gt;
&lt;td&gt;7 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutex lock/unlock&lt;/td&gt;
&lt;td&gt;25 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch from main memory&lt;/td&gt;
&lt;td&gt;100 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;send 2K bytes over 1Gbps network&lt;/td&gt;
&lt;td&gt;20,000 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read 1MB sequentially from memory&lt;/td&gt;
&lt;td&gt;250,000 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch from new disk location (seek)&lt;/td&gt;
&lt;td&gt;8,000,000 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read 1MB sequentially from disk&lt;/td&gt;
&lt;td&gt;20,000,000 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;send packet US to Europe and back&lt;/td&gt;
&lt;td&gt;150,000,000 nanosec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Recall Douglas Engelbart&apos;s &lt;a href=&quot;https://www.youtube.com/watch?v=UhpTiWyVa6k&quot;&gt;1968 collaborative editing demo&lt;/a&gt;, the &quot;mother of all demos&quot;, in which multiple editors at terminals connected to a mainframe worked on the same document.&lt;/p&gt;

&lt;p&gt;Imagine that the mainframe was in Europe while the terminals were in the California SRI building. Each keystroke would be one round trip: terminal sends signal to Europe (75ms), mainframe processes and sends back (75ms). Total: 150ms per keystroke.&lt;/p&gt;

&lt;p&gt;In the typing latency demo above, my threshold of perception is about 10ms; my threshold of tolerability is about 30ms. 150ms feels like typing in sand.&lt;/p&gt;

&lt;p&gt;In Engelbart&apos;s actual setup, which had a dumb terminal and the mainframe in the same building, latency was imperceptible — likely under 10ms. The collaborative editing task worked because operations were local.&lt;/p&gt;

&lt;p&gt;This principle is ubiquitous. Tasks that work at one latency become impossible at another.&lt;/p&gt;

&lt;p&gt;Consider a program manager in a software company who learns to code not to write production software, but to enable real-time thinking. Suppose he&apos;s in a conversation with an engineer, and his goal is to bring clarity to a plan for building a component. The engineer is being vague. If the program manager can think based on his own local knowledge about how such a component will work, he can ask focused questions and propose reasonable approximations. If not, he can only depend on what he gets from the engineer. The software engineering knowledge is available to him, just not locally. To utilize it he&apos;d have to say something like, &quot;let me spend a couple weeks reading up on this, then I can make some reasonable approximations.&quot; The latency makes the collaborative planning impossible.&lt;/p&gt;

&lt;p&gt;Local is faster. Sometimes that&apos;s nice to have, sometimes it&apos;s a sine qua non.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Local is constrained</title>
    <link href="https://mattobrien.org/local-is-constrained/" rel="alternate" type="text/html"/>
    <id>https://mattobrien.org/local-is-constrained/</id>
    <published>2025-11-04T04:36:00.000Z</published>
    <updated>2025-11-04T04:36:00.000Z</updated>
    <summary>The local-first computing movement arose as a corrective to cloud dependence, but local is constrained. Sometimes remote is better.</summary>
    <content type="html">&lt;p&gt;The local-first computing movement arose as a corrective to cloud dependence. Keep your data on your device, work offline, own your data and your software. All good things.&lt;/p&gt;

&lt;p&gt;But local is constrained. Your local machine has limited storage and compute. You can&apos;t store all of the web locally. The hardware you can buy is only as capable as the state of the art today.&lt;/p&gt;

&lt;p&gt;So practically speaking you cannot do everything locally that you might want. And for some operations, remote is actually preferred, even if you did have all the storage/compute/data/money in the world locally.&lt;/p&gt;

&lt;p&gt;Consider AI models. You&apos;d prefer to run them locally. Lower latency, no API costs, works offline, guaranteed privacy. But even if you could get the frontier labs to give you their model weights, your laptop certainly doesn&apos;t have the juice to run frontier models locally. And you probably don&apos;t want to buy (and carry around) a machine beefy enough to get close. Economically it makes sense for the inference to run in a hyperscaler&apos;s datacenter.&lt;/p&gt;

&lt;p&gt;Consider backups. You could store your backups locally. A local backup protects against software failures. But if your computer blows up, your local backup blows up too. The value you actually want — protection from catastrophic loss — requires offsite storage. The best backup service is remote. Geographic separation is the feature.&lt;/p&gt;

&lt;p&gt;Consider document collaboration. When you collaborate with someone else, they are remote to you. Local multiplayer — two people working on the same machine — is an edge case. Remote collaboration is what&apos;s wanted. It&apos;s inherently remote.&lt;/p&gt;

&lt;p&gt;Local gives you real benefits — speed, offline access, control, privacy. But sometimes remote is a practical necessity, and sometimes it&apos;s the actual benefit.&lt;/p&gt;</content>
  </entry>
</feed>
