Dev.to AI 🤖 Ai 👁 0 📖 5 min read

Which Coding Agent Is Waiting on You? Wiring Claude Code and Codex Into One Status Board on a Mac

Running two or three coding agents at once sounds efficient until you count the dead time. One terminal finished four minutes ago and is sitting at a prompt. Another stopped to ask whether it may run npm install, and you

Which Coding Agent Is Waiting on You? Wiring Claude Code and Codex Into One Status Board on a Mac

Running two or three coding agents at once sounds efficient until you count the dead time. One terminal finished four minutes ago and is sitting at a prompt. Another stopped to ask whether it may run npm install, and you never saw the question because that tab was behind your editor. The third is still working, so you keep checking it, which is the only one that did not need you.

The expensive part is not the model. It is the gap between "the agent needs a human" and "the human notices." This post shows how to close that gap with the hooks both tools already ship, using nothing but a shell script, jq, and a folder of small JSON files. You can copy it as is. At the end I show the view I use for the same job, but nothing here depends on it.

The three states that matter

Every session you care about is in one of three states:

  1. Working. Leave it alone.
  2. Needs you. It is blocked on a permission prompt or a question. Every minute here is wasted.
  3. Done, your turn. It finished the turn and wants review or the next instruction.

A useful board only has to answer one question: which sessions are in state 2 or 3, and in which folder?

What Claude Code gives you

Claude Code runs hooks at fixed points in a session. A hook is a command that receives JSON on stdin. Every event includes session_id, cwd and hook_event_name, which is exactly what a board needs to tell sessions apart.

Three events map onto the three states:

  • UserPromptSubmit fires when a prompt is submitted. That means working.
  • Notification fires when Claude Code sends a notification. Its matcher is the notification type, and the documented types include permission_prompt and idle_prompt. That means needs you.
  • Stop fires when Claude finishes responding. That means done, your turn.

Put this in ~/.claude/settings.json so it applies to every project on the machine (a project's .claude/settings.json works too, but then it only covers that repo):

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "~/.agent-status/report.sh working" }] }
    ],
    "Notification": [
      { "matcher": "permission_prompt|idle_prompt",
        "hooks": [{ "type": "command", "command": "~/.agent-status/report.sh needs_you" }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "~/.agent-status/report.sh done" }] }
    ]
  }
}

The reporter script

One file per session, overwritten on every event. No database, no daemon.

#!/bin/bash
# ~/.agent-status/report.sh <state>
state="$1"
input=$(cat)
dir="$HOME/.agent-status/sessions"
mkdir -p "$dir"

id=$(jq -r '.session_id // empty' <<<"$input")
[ -z "$id" ] && exit 0

jq -nc \
  --arg state "$state" \
  --arg agent "claude" \
  --arg cwd "$(jq -r '.cwd // ""' <<<"$input")" \
  --arg msg "$(jq -r '.message // ""' <<<"$input")" \
  --arg at "$(date +%s)" \
  '{state:$state, agent:$agent, cwd:$cwd, msg:$msg, at:($at|tonumber)}' \
  > "$dir/$id.json.tmp" && mv "$dir/$id.json.tmp" "$dir/$id.json"

if [ "$state" = "needs_you" ]; then
  osascript -e "display notification \"$(basename "$(jq -r '.cwd // ""' <<<"$input")") needs you\" with title \"Claude Code\""
fi
exit 0

Three details that save debugging time:

  • Write to a temp file, then mv. A reader that lists the folder mid write will otherwise hit half a JSON object. mv within one volume is atomic.
  • Exit 0 and stay fast. Hooks run inline with the session. A slow hook is a slow agent.
  • Don't write to /dev/tty. The docs are explicit that hooks run without a controlling terminal. If you want a terminal-native alert instead of osascript, return a terminalSequence field from the hook and Claude Code writes the escape sequence for you, in an interactive session.

chmod +x ~/.agent-status/report.sh and start a session. You should see a file appear in ~/.agent-status/sessions/ on your first prompt.

What Codex gives you, and what it doesn't

Codex has a notify setting that runs an external program with a JSON argument. The documented catch: it currently fires for one event, agent-turn-complete. The payload includes type, thread-id, turn-id, cwd, input-messages and last-assistant-message.

So Codex can tell your board done, keyed by thread-id, but notify cannot tell it needs you. Add it to your user-level ~/.codex/config.toml (Codex ignores notify in a project-local config and warns about it):

notify = ["/bin/bash", "-c", "~/.agent-status/codex.sh \"$1\"", "codex-notify"]
#!/bin/bash
# ~/.agent-status/codex.sh '<json>'
payload="$1"
[ "$(jq -r '.type' <<<"$payload")" = "agent-turn-complete" ] || exit 0
id=$(jq -r '."thread-id"' <<<"$payload")
dir="$HOME/.agent-status/sessions"; mkdir -p "$dir"
jq -nc --arg cwd "$(jq -r '.cwd // ""' <<<"$payload")" --arg at "$(date +%s)" \
  '{state:"done", agent:"codex", cwd:$cwd, msg:"", at:($at|tonumber)}' \
  > "$dir/codex-$id.json.tmp" && mv "$dir/codex-$id.json.tmp" "$dir/codex-$id.json"

For approvals, lean on the TUI instead. Codex's built-in tui.notifications can filter by event type, including approval-requested, and tui.notification_condition controls whether it fires only when the terminal is unfocused. That gives you an alert, not a board entry, which is an honest limit worth knowing before you build around it.

Reading the board

#!/bin/bash
# ~/.agent-status/board
now=$(date +%s)
for f in ~/.agent-status/sessions/*.json; do
  [ -e "$f" ] || continue
  jq -r --argjson now "$now" \
    '[.state, .agent, (.cwd|split("/")|last), (($now-.at)/60|floor|tostring)+"m ago"] | @tsv' "$f"
done | sort | column -t

Sorting puts done and needs_you above working alphabetically, which happens to be the order you want. Run it with watch -n 5 ~/.agent-status/board in a spare pane, or bind it to a key.

Two cleanup rules keep it honest:

  • Expire stale files. A session you closed with Ctrl+C never fires Stop. Drop anything older than a few hours: find ~/.agent-status/sessions -mmin +240 -delete.
  • Treat "working" as a claim, not a fact. It only means the last event was a prompt. If a process crashed, the file still says working. Age is your sanity check.

Where this breaks

  • Cloud sessions. Claude Code's docs note that cloud sessions don't read your local ~/.claude/settings.json, so those sessions never reach your board.
  • Subagents. Tool hooks also fire inside subagents, with agent_id set. If you later add PreToolUse reporting, key on it or one subagent will overwrite the parent's state.
  • Codex approvals. Covered above: notify will not see them.

The version I look at

I build TokenBar, a macOS 14+ menu bar app that shows usage and reset times for Claude, Codex, Cursor, Gemini and Copilot. It is built on Peter Steinberger's MIT-licensed CodexBar. It also has a panel for Pixel Agents, a separate MIT open-source project that draws coding sessions as people at desks. The panel connects to your own local Pixel Agents build (Node.js 20 or newer) rather than bundling it.

TokenBar Pixel Agents panel showing desks labeled working, done and needs your OK, with a Needs You list on the right. Demo mode with fictional projects.

The screenshot is demo mode with fictional projects. It sorts the same three states this post builds by hand: who is working, who is done and waiting for your turn, and who needs your OK. If the script above is enough for you, keep the script. TokenBar is $5/month Basic or $15 lifetime Pro and does not add any quota to your plans.

Disclosure: I build TokenBar.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.