fix(hooks): detect subagents from hook stdin (agent_id / agent_type, SessionStart source=fork), drop the fork feature-flag test - #1958
Open
theNetworkChuck wants to merge 1 commit into
Conversation
`isSubagentContext()` tested `CLAUDE_CODE_FORK_SUBAGENT === '1'` as a fork marker. That variable is the fork feature-ENABLE flag: settings.system.json sets it in the global env block and Claude Code >= 2.1.232 sets it in every interactive session, so every main session classified as a subagent and nine hooks silently no-op'd (danielmiessler#1864, danielmiessler#1911, PR danielmiessler#1942). Dropping the row alone re-opens danielmiessler#1831: a fork's environment is byte-identical to its parent's, so no env var can mark one. The marker that does is the hook's stdin — the documented `agent_id`/`agent_type` fields (a fork arrives as `agent_type: "fork"`) and SessionStart's `source: "fork"`. - lib/subagent.ts: remove the fork-flag row; add isSubagentHookInput(input) and an optional `input` parameter on isSubagentContext() (env union OR stdin stamp). Zero-arg callers are unchanged. - ISASync, SystemChangeSurface, ConfigEvalFire, MemoryReviewFire, MemoryTurnStart, KittyEnvPersist pass their parsed stdin to the guard; LoadContext adds a best-effort stdin read for the same purpose. - lib/hook-io.ts: document the optional agent_id / agent_type / source fields. Verified with the flag set in the environment: main-session input runs, fork- and delegate-stamped input skips, across Stop / SessionStart / PostToolUse shapes copied from real hook input. Refs danielmiessler#1831 danielmiessler#1864 danielmiessler#1911 danielmiessler#1942
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
isSubagentContext()keys on environment variables. That cannot work for forked subagents, and the workaround it shipped with (CLAUDE_CODE_FORK_SUBAGENT === '1') turned every main session into a "subagent" and silently disabled nine hooks (#1864, #1911, PR #1942).This PR removes the fork-flag row (same hunk as #1942) and replaces it with the signal that actually distinguishes a fork: the hook's stdin JSON. Consumers that already parse stdin pass it to the guard; the two SessionStart consumers that did not read stdin now do, best-effort.
Why an env var can never mark a fork
Measured on Claude Code 2.1.241 by spawning a fork and dumping its environment from inside: a fork's environment is byte-identical to its parent's — same
CLAUDE_CODE_SESSION_ID, sameCLAUDE_PID, noCLAUDE_AGENT_TYPE/CLAUDE_CODE_SUBAGENT_NAME/CLAUDE_CODE_SUBAGENT_TYPE, andCLAUDE_CODE_FORK_SUBAGENT=1in both. Nothing inprocess.envseparates them, so with the flag row removed (#1942) the original #1831 leak (forks re-injecting main-session context) is open again.What does mark a fork
The hooks reference documents
agent_idandagent_typeas common input fields "for subagent hooks", and listsforkas aSessionStartmatcher (i.e.source: "fork"on that event's input). Observed on the same 2.1.241 fork: its PostToolUse input carriesagent_type: "fork"plus anagent_id; an ordinary delegate carriesagent_type: "general-purpose"; main-session input carries neither.Change
lib/subagent.tsisSubagentContext(input?: unknown)— optional parsed-stdin argument; result is the existing env union OR the stdin stamp. Every zero-arg call site keeps compiling and behaves as before, minus the false positive.isSubagentHookInput(input)— true whenagent_idoragent_typeis a non-empty string, orhook_event_name === "SessionStart"withsource === "fork". Pure, zero-dep, never throws.Consumers now pass their parsed stdin:
ISASync,SystemChangeSurface(HookInputgains optionalagent_id/agent_type),ConfigEvalFire,MemoryReviewFire,MemoryTurnStart,KittyEnvPersist(guard moved after its existing stdin read;sourcecomment now includesfork),LoadContext(adds a best-effort stdin read).LoadMemoryandMemoryDeltaSurfaceare unchanged: their guarded paths are standalone probes, and their real callerMemoryTurnStartis now guarded on stdin.lib/hook-io.ts—HookInputdocuments the optionalagent_id,agent_type,sourcefields.No new dependencies; hooks stay importable before
bun install.Verification
All runs with
CLAUDE_CODE_FORK_SUBAGENT=1in the environment (whatsettings.system.jsonand Claude Code ≥ 2.1.232 both set), against a throwaway HOME, using stdin shapes copied from real captured hook input:falsefor PostToolUse / Stop / SessionStart-startup; fork PostToolUse (agent_type: "fork") and SessionStartsource: "fork"→true; ordinary delegate stamp →true; each surviving env marker alone →true; null / string / array / empty-string / non-string /source: "fork"on a non-SessionStart event →false, none throw.MemoryReviewFirewrites its per-session state on a main Stop and writes nothing on a fork-stamped Stop;LoadContextruns onsource: "startup"and prints its skip line onsource: "fork";KittyEnvPersistsame split;SystemChangeSurface.run()returns its surface line for a main write andnullfor a fork-stamped one.bun build --target=bun --no-bundle).Relationship to open work: stacks on #1942 (the removal hunk is the same; merging either first leaves a trivial conflict on that one hunk). Closes the re-opened #1831 concern and the "what is the reliable fork marker" follow-up noted on #1864 / #1911.