Keep headless runs out of the panel
`claude -p` was showing up as a session. It prints one answer and exits: there is no input line, it cannot be blocked on you, and there is nowhere to walk over to. A script that runs a few dozen of them turned the panel into a flicker of chips that were gone before they could be read. The Agent SDK and editor integrations drive claude the same way and are covered by the same rule. The flag is looked for in the arguments of the already-identified claude process, by exact token, so nothing new has to be discovered -- the pid was resolved on every event anyway. That resolution moved from apply_event up into main, which is where the decision has to be made: a headless run is dropped before the state file is touched at all, so it never creates one and correspondingly never deletes one on SessionEnd. Its events still reach the debug log, otherwise "why is my session missing from the panel" would have nothing to answer with. Arguments are now read by splitting /proc/<pid>/cmdline on its NUL separators rather than on spaces. A prompt is an ordinary argument, and `claude "when do I need -p"` is an interactive session that keeps its chip; the old space-joined string could not tell the two apart. looks_like_claude takes the list too, which is what it always wanted -- it was splitting the joined string back apart itself. Verified end to end as well as in the classifier: a fake claude runs the hook as a child through /proc, with -p leaving no file and without -p leaving one. A real `claude -p` against the installed hook added nothing to the state directory.
This commit is contained in:
+38
-9
@@ -41,22 +41,51 @@ import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("h", sys.argv[1])
|
||||
h = importlib.util.module_from_spec(spec); spec.loader.exec_module(h)
|
||||
cases = [
|
||||
("/bin/sh -c /home/u/claude-code-gnome-extension/hooks/claude-status-hook.py ", False),
|
||||
("/home/u/.local/bin/claude --resume ", True),
|
||||
("bash /home/u/bin/claude ", True),
|
||||
("node /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js ", True),
|
||||
("/home/u/bin/zellij --server /run/user/1000/zellij/x ", False),
|
||||
("nvim /home/u/.claude/settings.json ", False),
|
||||
(["/bin/sh", "-c", "/home/u/claude-code-gnome-extension/hooks/claude-status-hook.py"], False),
|
||||
(["/home/u/.local/bin/claude", "--resume"], True),
|
||||
(["bash", "/home/u/bin/claude"], True),
|
||||
(["node", "/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"], True),
|
||||
(["/home/u/bin/zellij", "--server", "/run/user/1000/zellij/x"], False),
|
||||
(["nvim", "/home/u/.claude/settings.json"], False),
|
||||
]
|
||||
bad = 0
|
||||
for cmd, want in cases:
|
||||
got = h.looks_like_claude(cmd)
|
||||
print(("ok " if got == want else "FAIL ") + "claude in %r -> %s" % (cmd[:46], got))
|
||||
for argv, want in cases:
|
||||
got = h.looks_like_claude(argv)
|
||||
print(("ok " if got == want else "FAIL ") + "claude in %r -> %s" % (" ".join(argv)[:46], got))
|
||||
bad += got != want
|
||||
|
||||
# A one-shot run has no input line and no human in front of it. The prompt is
|
||||
# an ordinary argument, so a prompt that merely mentions -p must not count.
|
||||
headless = [
|
||||
(["claude"], False),
|
||||
(["claude", "--resume"], False),
|
||||
(["claude", "-p", "summarise this"], True),
|
||||
(["claude", "--print", "--output-format", "stream-json"], True),
|
||||
(["claude", "explain what -p does"], False),
|
||||
(["claude", "--permission-mode", "plan"], False),
|
||||
]
|
||||
for argv, want in headless:
|
||||
got = h.is_headless(argv)
|
||||
print(("ok " if got == want else "FAIL ") + "headless %r -> %s" % (" ".join(argv)[:46], got))
|
||||
bad += got != want
|
||||
sys.exit(1 if bad else 0)
|
||||
PY
|
||||
check "command lines classified correctly" "0" "$?"
|
||||
|
||||
# End to end, through /proc rather than through the classifier: a fake "claude"
|
||||
# runs the hook as a child, exactly as the real one does.
|
||||
FAKE="$XDG_STATE_HOME/claude"
|
||||
printf '#!/bin/sh\n"$1"\n' > "$FAKE"
|
||||
chmod +x "$FAKE"
|
||||
printf '{"session_id":"headless","hook_event_name":"SessionStart","cwd":"/tmp/p"}' \
|
||||
| "$FAKE" "$HOOK" -p
|
||||
[ -e "$DIR/headless.json" ]; check "claude -p leaves no state file" "1" "$?"
|
||||
|
||||
printf '{"session_id":"headless","hook_event_name":"SessionStart","cwd":"/tmp/p"}' \
|
||||
| "$FAKE" "$HOOK"
|
||||
[ -e "$DIR/headless.json" ]; check "the same session without -p is recorded" "0" "$?"
|
||||
rm -f "$DIR/headless.json" "$DIR/headless.json.lock"
|
||||
|
||||
# --- state machine ---------------------------------------------------------
|
||||
emit "$(ev SessionStart '"source":"startup"')"
|
||||
check "SessionStart -> waiting" "waiting" "$(field state)"
|
||||
|
||||
Reference in New Issue
Block a user