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.