Skip to content

Pin spawned Claude Code CLI to compiled-in feature-gate defaults - #957

Open
jhampton wants to merge 1 commit into
craft-ai-agents:mainfrom
Swagatar-LLC:contrib/growthbook-determinism
Open

Pin spawned Claude Code CLI to compiled-in feature-gate defaults#957
jhampton wants to merge 1 commit into
craft-ai-agents:mainfrom
Swagatar-LLC:contrib/growthbook-determinism

Conversation

@jhampton

@jhampton jhampton commented Jul 8, 2026

Copy link
Copy Markdown

Problem

The spawned Claude Code CLI resolves feature gates through a remote GrowthBook config. One of those gates (tengu_amber_heron, compiled default off) controls whether a Task subagent launches synchronously (blocking until the child finishes) or asynchronously (fire-and-forget).

The launch decision reduces to:

isAsync = isRemote
  || run_in_background === true
  || agentDef.background === true
  || teamsMode || tasksMode
  || (!isTeammate && run_in_background !== false && featureGate("tengu_amber_heron", false))

When that remote gate flips on, subagents launch async by default even when run_in_background was never set. Async launches live inside the SDK subprocess and are torn down when it is disposed/recreated, and callers that only track explicitly backgrounded tasks never see them. The net effect: spawned-CLI behavior can change mid-flight — between two runs of the same code, same CLI version — purely because a remote flag was toggled. That is a reproducibility hazard for any host that relies on blocking subagent semantics.

Fix

Set DISABLE_GROWTHBOOK in buildClaudeSubprocessEnv() so the spawned CLI resolves every feature gate to its compiled-in default. Behavior becomes deterministic and independent of remote config; the async-launch gate stays at its compiled default (off) and cannot silently flip.

Notes:

  • Explicit run_in_background: true still works — this only pins the default. It is deliberately narrower than CLAUDE_CODE_DISABLE_BACKGROUND_TASKS, which removes the run_in_background parameter from the tool schemas entirely.
  • An existing DISABLE_GROWTHBOOK value (from the environment or envOverrides) is respected, so operators can opt back into remote gates.
  • More broadly, this removes remote-config influence over spawned-CLI behavior — the subprocess behaves the same offline as online.

Test plan

Adds packages/shared/tests/claude-subprocess-feature-gates.test.ts:

  • pins DISABLE_GROWTHBOOK=1 so feature gates resolve to compiled defaults.
  • respects a DISABLE_GROWTHBOOK value already present in the environment.
  • respects a DISABLE_GROWTHBOOK value passed via envOverrides.

Validation:

  • bun test tests/claude-subprocess-feature-gates.test.ts in packages/shared → 3 pass, 0 fail.
  • tsc --noEmit on packages/shared → 0 errors.

Whether a Task subagent launches synchronously (blocking) or
asynchronously (fire-and-forget) is controlled by a remote GrowthBook
feature gate (tengu_amber_heron, compiled default off). When that gate
flips on, subagents launch async by default even when run_in_background
was never set. Async launches live inside the SDK subprocess and are
torn down when it is disposed, and callers that track only
explicitly-backgrounded tasks never see them — so subprocess behavior
can change mid-flight purely from remote config, with no code or version
change.

Set DISABLE_GROWTHBOOK in buildClaudeSubprocessEnv() so the spawned CLI
resolves every gate to its compiled-in default. This makes subprocess
behavior reproducible and independent of remote config. Explicit
run_in_background: true still works, unlike
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS which removes the parameter
entirely. An existing DISABLE_GROWTHBOOK value is respected.

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant