fix(farm): scrub dispatcher secrets from the pluggable mutation hook env#143
Merged
Conversation
The pluggable FARM_MUTATION_CMD hook spawned with { ...process.env }, so an
operator-wired (third-party) mutation framework inherited FARM_API_KEY and
CLAUDE_CODE_OAUTH_TOKEN. It was the one child-spawn path that bypassed run()'s
least-privilege scrub, against the intent stated in run()'s own comment ("git,
operator gate/setup/test, mutation never need the dispatcher's secrets").
Add a shared scrubbedEnv(extra?) helper in exec.ts as the single source of
truth (merge caller vars onto process.env, then delete both secrets LAST so an
extra var cannot reintroduce one, CodeQL #5) and route both run() and the
mutation-hook spawn through it. The hook still receives the FARM_MUTATION_*
contract vars; only the credentials are stripped.
Proven by a subprocess test: the hook records its own env to a probe file; the
test asserts FARM_API_KEY/CLAUDE_CODE_OAUTH_TOKEN read ABSENT while the
FARM_MUTATION_* vars are present. Pre-existing gap surfaced by
auth-crypto-reviewer during the #140 (v2.rev.0020) review, not introduced there.
CHANGELOG: the pluggable mutation hook (FARM_MUTATION_CMD) no longer inherits
the dispatcher's API key or OAuth token, giving least-privilege parity with
every other child command.
Ref: v2.rev.0020 follow-up (auth-crypto-reviewer finding)
Claude-Session: https://claude.ai/code/session_0123LEtLuVCEzLMhWdktP7aC
Lock the security property that scrubbedEnv() deletes FARM_API_KEY / CLAUDE_CODE_OAUTH_TOKEN AFTER merging caller-supplied vars, so an `extra` entry can never reintroduce a scrubbed secret. The integration test proves the mutation hook is scrubbed but does not pass a secret in `extra`, so a future reorder of the delete before the merge would pass every test and silently break the defense. This direct unit test pins the ordering. Closes a coverage-auditor LOW finding on the env-scrub fix. Claude-Session: https://claude.ai/code/session_0123LEtLuVCEzLMhWdktP7aC
SUaDtL
added a commit
that referenced
this pull request
Jun 27, 2026
* docs(changelog): roll the 2.6.0 release notes Reconcile the [2.6.0] CHANGELOG section for the release: broaden the intro beyond the farm work, add context-drift provenance (#145) and file-scoped context injection (#146) under Added, note the MIT to AGPLv3 relicense (#147, ADR-0009) under Changed, and add a Fixed section for #143, #139, and #138. Dated 2026-06-27. Also flips v2.feature.0001 to done now that file-scoped context injection has shipped and merged; the flip rides this release commit per ADR-0008. No version bump: plugin.json is already 2.6.0 and the ca-scoped commit window is unchanged. The relicense and docs commits touch no plugins/ca/ payload. Claude-Session: https://claude.ai/code/session_01QaLw7nTrRbHwygFar2R6zu * docs(license): reserve the dual-licensing right without offering it yet Soften the README "Commercial licensing" wording. The prior text implied proprietary licenses were actively on offer; the intent is narrower: the owner retains sole ownership and reserves the right to dual-license, but is not offering commercial licenses at this time. Inquiries may still be sent through GitHub and will be considered if and when a commercial path is established. The legal substance is unchanged. AGPLv3 plus sole copyright already secures the right; this only corrects the public statement so it does not overstate an active commercial offering. ADR-0009, the CLA, and the CHANGELOG already use the reserves-the-right framing, so no other file changes. Claude-Session: https://claude.ai/code/session_01QaLw7nTrRbHwygFar2R6zu --------- Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
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.
What
Closes a least-privilege gap in the farm dispatcher: the pluggable
FARM_MUTATION_CMDmutation hook spawned with{ ...process.env }, so an operator-wired (possibly third-party) mutation framework inheritedFARM_API_KEYandCLAUDE_CODE_OAUTH_TOKEN. It was the one child-spawn path that bypassedrun()'s existing secret scrub, against the intent stated inrun()'s own comment.The fix adds a single shared helper in
exec.ts:Both
run()and the mutation-hook spawn now route through it, so there is one scrub, not two that can drift. The hook still receives theFARM_MUTATION_*contract vars; only the credentials are stripped.Why
This is the same least-privilege control
run()already applied to git, gate, setup, and test commands (CodeQL #5). The mutation hook runs operator-supplied code in the worktree and has no need for the dispatcher's API key. Deleting the secrets after mergingextrameans a caller var can never re-introduce one.Surfaced by
auth-crypto-reviewerduring the #140 (v2.rev.0020) review as a pre-existing gap, not introduced by that refactor. Conflict-hierarchy: a level-1 (security) tightening with no behavior change to any non-secret path.Test plan
farm.test.ts): the hook runs a committed probe that records its own env; the test assertsFARM_API_KEY/CLAUDE_CODE_OAUTH_TOKENreadABSENTwhileFARM_MUTATION_FILES/TEST_PATH/TEST_CMDare present. Fails on the pre-fix code (the secret leaks), passes after.farm.unit.test.ts): pins the delete-last ordering by passing a secret inextraand asserting it is stripped, so a future reorder cannot silently break the defense.run()scrub test still green (now exercises the shared helper).npx vitest run: 171/171.npx tsc --noEmit: clean.farm.jsrebuilt and deterministic.Reviews
All three path-matrix reviewers PASS, no CRITICAL or HIGH:
auth-crypto-reviewer:scrubbedEnv()removes both secrets after the merge;run()behavior identical; no real secret, TLS downgrade, or banned primitive.security-reviewer: tightens the secret-store control;SHELL_OPTScannot override the scrubbed env; the twosk-/tok-shaped values are fake test sentinels matching the sanctioned dummy-value convention.coverage-auditor: about 85 percent, bothscrubbedEnvpaths exercised, both obligations verified. Its one LOW (untested delete-last ordering) is closed by the unit test in this PR.Note on the commit gate
The compound-name secret detection from v2.rev.0016 (shipping in 2.6.0) flags the test's
FARM_API_KEY:/CLAUDE_CODE_OAUTH_TOKEN:fixture lines. The secret-handling gate reviewed them (fake sentinels) and recorded a diff-bound pass before commit. The currently installed plugin (2.5.2) predates that detection, so its local hook does not flag them, which is expected release lag, not a gap.Ref: v2.rev.0020 follow-up (auth-crypto-reviewer finding)