>>> claude code hooks, put to work Claude Code Hooks. Know When Claude Needs You

Claude Code hooks are shell commands Claude Code runs at fixed moments: a prompt arrives, a tool runs, a permission is needed, a turn ends. Here's every event, a working setup that tracks whether Claude is working, waiting or done, and the four traps we hit building exactly that into RETRO TMUX.

tl;dr.txt— less —[ 1/1 ]

# hooks live in ~/.claude/settings.json. see what's installed:

$ claude

› /hooks

# Stop ≠ done. SessionStart ≠ fresh. idle_prompt ≠ a question. details below.

// The events //

Seven Moments Claude Tells You About

Each hook gets the event as JSON on stdin (session id, transcript path, working folder, plus fields for that event) and runs with the pane's environment, so $TMUX_PANE tells you which tmux pane Claude is in. These are the ones that matter for knowing what Claude is doing:

EventFires whenWhat it tells you
SessionStartClaude starts, resumes, clears, or compactsIdle and ready, unless source is compact
UserPromptSubmitYou send a promptWorking
PreToolUseRight before a tool runsWorking, and which tool: Bash · npm run migrate
PermissionRequestClaude is about to ask permissionNeeds you. The hook can answer allow or deny itself
NotificationClaude wants your attentionDepends on notification_type: a question, a permission, or just "done, come look"
StopA turn endsDone, unless background_tasks still has one running
SessionEndClaude exitsClear everything; nobody's home

// A Claude Code hooks example · 5 minutes //

Is Claude Done Yet? In 25 Lines

A Claude Code hooks example you can copy: a small script that turns hook events into one word per tmux pane, the raw material for Claude Code notifications: working, needs-you, done or idle. It already handles the traps below.

  1. 01

    Save the script

    Put it in ~/bin/claude-state.sh. It reads the event from stdin and writes the state as a tmux pane option, @claude_state.

    #!/bin/sh
    # claude-state.sh <Event>: what is Claude doing in this tmux pane?
    [ -n "$TMUX_PANE" ] || exit 0
    event=$1
    json=$(tr -d ' \t\r\n')     # the event payload, whitespace removed
    set_state() { tmux set -p -t "$TMUX_PANE" @claude_state "$1"; exit 0; }
    case $event in
      UserPromptSubmit|PreToolUse) set_state working ;;
      Stop) case $json in
              *'"background_tasks":['*'"status":"running"'*) set_state working ;;
              *) set_state done ;;
            esac ;;
      Notification) case $json in
              *'"notification_type":"permission_prompt"'*) set_state needs-you ;;
              *'"notification_type":"elicitation_dialog"'*) set_state needs-you ;;
              *'"notification_type":"idle_prompt"'*) set_state done ;;
            esac ;;
      SessionStart) case $json in *'"source":"compact"'*) exit 0 ;; esac
            set_state idle ;;
      SessionEnd) tmux set -p -u -t "$TMUX_PANE" @claude_state ;;
    esac
    exit 0
    you@server: ~
    $ chmod +x ~/bin/claude-state.sh
    (Claude asks for permission…)
    [agents] 0:claude*claude: needs-you
  2. 02

    Register the hooks

    Add this to ~/.claude/settings.json (merge it with anything already there). Every event runs the same script with its own name. Restart Claude Code, then check with /hooks.

    {
      "hooks": {
        "SessionStart":     [{ "hooks": [{ "type": "command", "command": "sh ~/bin/claude-state.sh SessionStart", "timeout": 5 }] }],
        "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "sh ~/bin/claude-state.sh UserPromptSubmit", "timeout": 5 }] }],
        "PreToolUse":       [{ "hooks": [{ "type": "command", "command": "sh ~/bin/claude-state.sh PreToolUse", "timeout": 5 }] }],
        "Notification":     [{ "hooks": [{ "type": "command", "command": "sh ~/bin/claude-state.sh Notification", "timeout": 5 }] }],
        "Stop":             [{ "hooks": [{ "type": "command", "command": "sh ~/bin/claude-state.sh Stop", "timeout": 5 }] }],
        "SessionEnd":       [{ "hooks": [{ "type": "command", "command": "sh ~/bin/claude-state.sh SessionEnd", "timeout": 5 }] }]
      }
    }
  3. 03

    Show it in the status bar

    Add one line to ~/.tmux.conf. It shows the state of the pane you're looking at. Which is the catch: you have to be looking.

    set -g status-right "claude: #{@claude_state} "

// The traps //

Four Ways Your Status Script Lies

We learned these building agent status into RETRO TMUX and testing it against Claude Code 2.1. Most hook examples online fall into at least two, which is why so many Claude Code notifications arrive too early, too late, or for the wrong thing.

01 · Stop Isn't Done

Stop fires when a turn ends, even if Claude left a background task running. The payload lists them in background_tasks; one with "status":"running" means Claude is still working. Ping your phone on every Stop and you'll get "done" for a build that's still compiling.

02 · SessionStart Isn't Fresh

Claude fires SessionStart again after a compaction, with "source":"compact", and it can happen in the middle of a turn. Reset to idle there and a busy Claude looks asleep.

03 · idle_prompt Isn't A Question

Notification covers three different things. permission_prompt and elicitation_dialog need an answer. idle_prompt just means a finished result is waiting to be read: that's done, not needs-you.

04 · The State Outlives Claude

Quit Claude with Ctrl-C twice, or kill it, and whatever you wrote last sticks around. Clear on SessionEnd, and don't trust a state unless the process that wrote it is still in the pane. Fun detail: Claude Code renames its own process to its version number, like 2.1.280, so pgrep claude won't find it.

Bonus · Keep Hooks Fast

Hooks run inline. A hook that talks to the network makes Claude wait for it, so write a file or a tmux option and get out. Give every hook a short timeout; ours use 5 seconds.

Bonus · Don't Clobber

People already have hooks and a status line in settings.json. Merge, never overwrite, and keep a backup. Your future self, uninstalling at 2am, says thanks.

// Or skip all of this //

One Switch. Status On Your Phone

A status bar only helps if you're at the desk, looking at the right pane. RETRO TMUX installs these hooks for you with one switch, for Claude Code, Codex and Hermes, and shows every agent's state on your iPhone and iPad, across every session, without attaching.

>>> SESSIONS203.0.113.7
    CLAUDE USAGE 42%read 2m ago

    • All four traps handled, plus a check that the agent that wrote the state is still running.
    • Merges into your settings, keeps a backup, and uninstalls byte for byte.
    • Keeps your own status line and adds your Claude usage to the phone.
    • A push when Claude needs you, with Approve and Deny on the notification. Your answer reaches Claude within seconds.
    • The Dynamic Island, one live card per agent window, and Reply with a typed answer from the lock screen.

    > Codex hooks, and its review screen → · Claude Code on your iPhone →

    // FAQ //

    Claude Code Hooks, Answered

    What are Claude Code hooks?

    Shell commands Claude Code runs at fixed moments in its life: when a session starts, when you send a prompt, before a tool runs, when it asks permission, when it stops, when it notifies you, and when the session ends. Each hook gets the event as JSON on stdin, so a small script can react to what Claude is doing.

    Where do Claude Code hooks go?

    In a settings file: ~/.claude/settings.json for every project, or .claude/settings.json inside one project. Under "hooks", each event name holds a list of groups, and each group holds the commands to run. Type /hooks inside Claude Code to see what's installed.

    How do I get a Claude Code notification when it's done?

    Use the Stop hook, but check the payload's background_tasks first: if one is still running, Claude isn't finished. The Notification hook with notification_type idle_prompt also means the turn is done and waiting to be read. RETRO TMUX does this for you and shows it on your iPhone.

    Why does my Stop hook fire while Claude is still working?

    Because Stop marks the end of a turn, not the end of the work. If Claude left a background task running, the Stop payload lists it in background_tasks with "status":"running". Treat that as still working.

    Does SessionStart run after /compact?

    Yes. Claude fires SessionStart after a compaction too, with "source":"compact", sometimes in the middle of a turn. If your SessionStart hook resets state to idle, skip that source or you'll mark a busy Claude as idle.

    Can I remove hooks cleanly?

    Delete the entries from settings.json, or let a tool that keeps a backup do it. RETRO TMUX only ever touches its own entries, keeps a copy of your file, and restores it byte for byte on uninstall when nothing else changed.

    >>> READY WHEN YOU ARE

    claude --hooks=on

    RETRO TMUX is in beta. Join, and we'll tell you when there's a build to break.

    + JOIN THE BETA

    More: what RETRO TMUX does · Codex hooks · Claude Code on iPhone

    join-beta.sh— retro tmux —

    >>> BETA ACCESS

    Join The Beta

    Tell us a little about your setup. We'll email you when there's a build to break. No newsletter, no spam, just the invite.

    you'll use it on
    agents you run
    your server