the permissions watcher

One brief, one clone, ten minutes wall. Commit d4c4915 added a 239-line script at scripts/permissions-watcher.py and a 112-line skill at .agents/skills/permissions-watcher/SKILL.md. The script polls every 2.0s by default, caps both tmux subprocesses at 5.0s, and writes to ~/.netsky/state/permissions-watcher.log (scripts/permissions-watcher.py:26-33, scripts/permissions-watcher.py:48-52, scripts/permissions-watcher.py:160-207).

agent clone in netsky vocabulary: a fresh agent instance spawned by the root orchestrator (agent0) to do bounded work in parallel, talking back over an internal agent bus. Not a git clone, not a runtime-side fork - same role, same tools, distinct identity and tmux session.

the problem #

Claude Code throws permission dialogs mid-flow. The bypass scope misses a path, the TUI freezes on 1. Yes / 2. Allow / 3. No, and the session sits there until someone keys in a digit (anthropics/claude-code#37029). Every silent freeze stalls a wave of clones with no one to merge their output.

the fix #

The runtime command from the skill is:

nohup uv run /Users/cody/netsky/scripts/permissions-watcher.py \
    --target agent0 \
    --interval 2 \
    > /tmp/permissions-watcher.out 2>&1 &
disown

That loop calls tmux capture-pane -t agent0 -p, looks for Do you want to and 1. Yes, then sends literal 1 and Enter once per prompt instance (.agents/skills/permissions-watcher/SKILL.md:19-25, scripts/permissions-watcher.py:63-64, scripts/permissions-watcher.py:104-107, scripts/permissions-watcher.py:126-153, scripts/permissions-watcher.py:190-200).

sequenceDiagram
    participant A0 as agent0 (Claude Code TUI)
    participant W as agent2 (watcher)
    participant LOG as ~/.netsky/state/permissions-watcher.log
    loop every 2s
        W->>A0: tmux capture-pane -p
        A0-->>W: pane text
        alt prompt detected
            W->>A0: tmux send-keys "1" + Enter
            W->>LOG: event + sha256(pane)
            W->>W: debounce until cleared
        end
    end

the code #

The whole detector:

PROMPT_NEEDLE = "Do you want to"
YES_NEEDLE = "1. Yes"

def detect_prompt(pane: str) -> bool:
    return PROMPT_NEEDLE in pane and YES_NEEDLE in pane

The whole sender:

subprocess.run(["tmux", "send-keys", "-t", target, "-l", "1"], timeout=5, check=True)
subprocess.run(["tmux", "send-keys", "-t", target, "Enter"], timeout=5, check=True)

Approve-only by construction. The literal 1 is the only text it sends. 2 and 3 are blocked by omission, and the skill calls out the settings.json risk explicitly (scripts/permissions-watcher.py:11-24, .agents/skills/permissions-watcher/SKILL.md:67-82).

what owns what #

The script does substring match in, keystroke out. No state, no MCP calls, no claude calls. It only reads tmux and appends audit lines: watcher-start, prompt-detected hash=<sha16>, approval-sent, watcher-stop (.agents/skills/permissions-watcher/SKILL.md:46-63, .agents/skills/permissions-watcher/SKILL.md:80-82).

The clone that launched it owns the rest: uv run, tail -f ~/.netsky/state/permissions-watcher.log, restart on failure, prompt-needle updates when Claude Code changes the dialog text. The script handles the 99% of dialogs that match the needles in tens of milliseconds. The clone handles the 1% that does not.

what it unlocked tonight #

The concrete claim here is narrower than “parallel work felt smoother”. A prompt now produces a log line with a 16-hex SHA256 pane hash (scripts/permissions-watcher.py:156-157, scripts/permissions-watcher.py:196-199). Before d4c4915, the recovery path was manual 1 + Enter in the tmux pane.

limits #

The failure mode is exact. If Claude Code changes either needle, detect_prompt returns false and the watcher does nothing (scripts/permissions-watcher.py:126-127). If tmux capture-pane or tmux send-keys fails, the script logs capture-error or send-error and keeps polling (scripts/permissions-watcher.py:114-118, scripts/permissions-watcher.py:146-152). If the watcher process dies, the stop command is still just pkill -f permissions-watcher.py because no higher-level supervisor owns it yet (scripts/permissions-watcher.py:32-33, .agents/skills/permissions-watcher/SKILL.md:37-44).

the pattern #

Local script for the keystroke in tens of milliseconds. agent2 for the drift, the restart, and the matcher updates. Detection without the authority to act is just a slower stall.