Skip to content

discipline:sweep-all fans out fork subagents with no runtime check that fork mode is available #1670

Description

@kyle-sexton

Filed by an automated plugin-quality audit (session-based, not a maintainer) — see reasoning and verify independently before acting; this is a report, not a mandate.

Audited: discipline 0.9.0, read from ~/.claude/plugins/cache/melodic-software/discipline/0.9.0/. claude plugin validate passed clean.

Finding (IMPORTANT) — no runtime preflight before fanning out forks

Location: skills/sweep-all/SKILL.md step 1 (Fan out, audit-only); cross-referenced against skills/setup/SKILL.md step 6.

The skill's own claim (step 1): fork-spawning is a rollout-gated capability (CLAUDE_CODE_FORK_SUBAGENT); so if forks are unavailable, the text says to report that the inheriting audit fan-out cannot run and stop, rather than auditing blind. That sentence sits inline in the fan-out step's prose — there is no separate numbered pre-step that gates the loop on a check before spawning. setup/SKILL.md step 6 separately reports the same fact as an install-time INFO note (where it is off, only the session-start posture digest runs), but that is a one-time setup report a user may never re-read, not a runtime invariant sweep-all itself enforces per invocation.

Observed: in the audited session, CLAUDE_CODE_FORK_SUBAGENT was unset (confirmed via echo, independently reproduced by the auditor in a fresh Bash invocation). Six subagent_type: fork agents were spawned in parallel before any check. The degradation was discovered only because one fork (sweep-follow-standards) self-reported back that its context held only the system prompt and task message — no parent turns came across. The other five were stopped once the pattern was recognized, but all six had already been spawned.

Doc citation: https://code.claude.com/docs/en/sub-agents — "Fork the current conversation" section (fetched 2026-07-26). Verbatim:

"Before v2.1.212, the forked-subagent command was /fork. It was enabled by default on v2.1.161 or later; on v2.1.117 through v2.1.160 it required setting the CLAUDE_CODE_FORK_SUBAGENT environment variable to 1, unless a server-side rollout enabled it... This capability may also be enabled in interactive sessions as part of a staged rollout."

"Setting CLAUDE_CODE_FORK_SUBAGENT=1 enables fork mode... setting it to 0 disables fork mode everywhere, including any server-side rollout."

The important nuance the doc adds: an unset value does not reliably mean "off" — it defers to an undocumented server-side staged rollout. A client-side env-var read is therefore a necessary-but-not-sufficient probe: it can prove "definitely off" (=0) or "definitely on" (=1), but "unset" is genuinely ambiguous from inside the session.

Failure scenario: the skill's own text states this outcome directly, not as a hedge — sweep-all/SKILL.md step 1: "Fork-spawning is a rollout-gated capability (CLAUDE_CODE_FORK_SUBAGENT); where it is off, requesting the fork type falls back to a fresh general-purpose subagent that cannot see the conversation — so if forks are unavailable, report that the inheriting audit fan-out cannot run and stop, rather than auditing blind." The skill already documents both the failure mode and the required response; the gap is purely that nothing enforces the check before the fan-out loop runs. Nothing in sweep-all detects this before spawning. The cost is the skill's own documented figure — roughly 170K tokens per fork (its Gotchas section), and up to ~1.4M tokens for an 8-in-scope pass run in two waves of four — multiplied by the fan-out width, plus a full round-trip of manual investigation before the fan-out can be aborted.

Severity: IMPORTANT. Not a correctness failure (the fork honestly self-reported; no audit result was silently trusted), but exactly the "silently expensive" class the audit lens flags: 6 wasted spawns before abort.

Candidate remediations (cheapest first)

  1. Cheapest, and the auditor's recommended adoption: insert an explicit step 0 in sweep-all's batch pass — dispatch ONE fork first as a canary, instruct it to report whether it can see prior conversation turns, and only fan out the remaining N-1 forks once that's confirmed. This is more robust than an env-var read precisely because "unset" is ambiguous per the doc above — a canary reflects the true runtime resolution (local override plus server rollout combined) rather than only the local override.
  2. Cheap adjunct, explicitly not a substitute: also read CLAUDE_CODE_FORK_SUBAGENT as a fast short-circuit — a 0 read can skip the canary and stop immediately at zero cost; a 1 or unset read still needs the canary. State this asymmetry explicitly in the skill text so a future editor doesn't simplify it back to an env-var-only check.
  3. More ambitious: promote setup/SKILL.md step 6's install-time awareness into something sweep-all itself re-checks every run, rather than trusting a report the user saw once, if ever.

Named assumptions and blindspots (stated as such, not as fact)

  • The exact silent-fallback mechanic (falls back to a fresh general-purpose subagent with empty context) is stated by sweep-all/SKILL.md itself, and corroborated by the audited session's own empirical observation (the self-reporting fork) — but it is not independently confirmed in the fetched official doc's own wording, which documents the CLAUDE_CODE_FORK_SUBAGENT gate and staged-rollout ambiguity but not the specific fallback-to-empty-context behavior in those terms. This review did not re-fetch the doc a second time to double-check that specific gap.
  • CLAUDE_CODE_FORK_SUBAGENT's true resolution in any given session also depends on an undocumented server-side staged-rollout state per the official doc's own wording ("may also be enabled... as part of a staged rollout"). The empirical echo check only proves the local env var is unset, not that fork mode is definitively off for that machine's sessions overall. This is a limit of the plugin's own detectable surface, not something the audit pass could close.
  • The auditor did not check whether discipline's other fan-out correctors (do-your-research-deep, recheck-against-upstream-deep) share sweep-all's exact capability-dependency pattern on forks — plausible given the shared method doc, but unverified. Worth a follow-up pass if this plugin is revisited.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: highSignificant impact, or blocks an imminent release; staff this cycle.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions