Reports

#4070 · Environment loading gap at provider startup

BugMedium priorityHigh effortprovidersworkspacesprovider-claude-code

GitHub issue · 2026-09-22 · trusted base 07a741bc2014561bf5721ba6dadbfc25c6eb3812

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: medium · Reproduction label: partial-repro

1. TL;DR

The provider startup path uses inherited process variables and explicit overrides; choosing a working directory does not load its environment files. A focused test reaches the real Claude bridge and captures the options sent to the mocked Claude SDK: a variable present only in a synthetic directory file is missing, while explicitly supplying the same variable succeeds. A separate test of the real plugin service, backed by an in-memory migrated database, confirms that 32 contributed entries survive and 33 entries cause the whole contribution to be discarded. Both results repeat in a second clean checkout. This is a component-level reproduction, not a reproduction of the reporter's actual MCP authentication failure.

2. Claims vs findings

ClaimFindingEvidence
Directory variables do not reach the agent.Partially verifiedThe bridge/SDK boundary test sees no directory-only marker. The upstream spawn path forwards inherited variables but has no directory loader. A real agent process was not launched.
Shell startup configuration can hide the missing agent environment.Unverified end to endNo shell-hook plugin or interactive shell was used. Shell startup controls do not themselves expand the parent environment.
MCP receives an unresolved substitution as a username.UnverifiedNo live Claude CLI, MCP server, database, or credentials were used. The missing-variable condition is supported; the exact substitution behavior was not tested.
The plugin contribution limit rejects large directory environments.VerifiedThe real service returns 32 entries for a 32-entry resolver and zero for a 33-entry resolver, including loss of its PATH entry.
Failure has no visible UI notification.Partially verifiedThe resolver catches the invalid result and continues without entries. UI notification behavior was not exercised.
Copied workspace files should affect provider environment.Not sufficient on its ownFile presence is reproduced synthetically; startup still needs a mechanism that evaluates or parses those files. Worktree-copy behavior was not tested.

3. Environment

4. Minimal reproduction

  1. Save the complete inline installer below as install-repro.py. It adds only two test blocks to trusted existing test harnesses; it does not change production code.
  2. Run the following from a clean checkout. The provider command deliberately returns exit 1; continue with the server command afterward.
    git clone https://github.com/get-bb/bb.git bb-4070
    cd bb-4070
    git checkout --detach 07a741bc2014561bf5721ba6dadbfc25c6eb3812
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
    python3 /path/to/install-repro.py "$PWD"
    pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- --testNamePattern='issue 4070' src/bridge/__tests__/bridge.test.ts
    pnpm exec turbo run test --filter=@bb/server -- --testNamePattern='issue 4070' test/services/plugins/plugin-agent-contributions.test.ts --silent=false
  3. The expected directory-inheritance assertion fails; the explicit override control passes. The observed-behavior assertion for the cap passes.
Provider, expected: directory-only marker = "workspace-sentinel"
Provider, actual: undefined
Provider test summary: 1 failed | 1 passed | 76 skipped

Plugin service, 32 entries: 32 retained
Plugin service, 33 entries: 0 retained; PATH absent
{"accepted":32,"oversized":0,"pathRetained":false}
Server test summary: 1 passed | 6 skipped

The synthetic variable has no credential value. The SDK is mocked only to capture its options without launching a paid agent. The bridge and session-option code are real. The server test installs a synthetic resolver through the normal plugin service. Its 33-entry assertion documents current behavior, rather than asserting that the contract must accept arbitrary environments.

Complete reproduction installer and test source
from pathlib import Path
import sys
root = Path(sys.argv[1])
p = root / 'plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts'
s = p.read_text()
test = '''
  it.each([false, true])("issue 4070: directory environment at SDK boundary, explicit=%s", async (explicit) => {
    const workspace = mkdtempSync(join(tmpdir(), "bb-env-repro-"));
    writeFileSync(join(workspace, ".envrc"), "export BB_REPRO_DIRECTORY=workspace-sentinel\\n");
    vi.stubEnv("BB_REPRO_DIRECTORY", undefined);
    const bridge = createBridgeJsonRpcTestHarness(handleLine);
    const query = createControlledClaudeQuery();
    queryMock.mockReturnValue(query);
    try {
      bridge.sendRequest(40701, "thread/start", {
        threadId: "repro-directory-environment",
        cwd: workspace,
        instructionMode: "append",
        options: {
          ...(explicit ? { envVars: { BB_REPRO_DIRECTORY: "workspace-sentinel" } } : {}),
          permissionMode: "accept-edits",
          permissionScope: "workspace",
          approvalReviewer: "user",
          permissionEscalation: "ask",
          instructions: "test",
          providerOptions: { workflowsEnabled: false },
        },
      });
      await bridge.waitForResponse(40701);
      const value = getLatestQueryOptions().env?.BB_REPRO_DIRECTORY;
      console.log(JSON.stringify({ explicit, sdkEnvironmentHasProbe: value !== undefined }));
      expect(value).toBe("workspace-sentinel");
    } finally {
      bridge.sendRequest(40702, "thread/stop", {
        threadId: "repro-directory-environment",
        providerThreadId: "repro-directory-environment",
        intent: "interrupt",
        activeTurnId: null,
      });
      await bridge.flushWork();
      query.finish();
      await bridge.waitForResponse(40702);
      bridge.restore();
      vi.unstubAllEnvs();
      rmSync(workspace, { recursive: true, force: true });
    }
  });
'''
anchor = 'describe("bridge", () => {'
assert s.count(anchor) == 1
p.write_text(s.replace(anchor, anchor + test, 1))
p = root / 'apps/server/test/services/plugins/plugin-agent-contributions.test.ts'
s = p.read_text()
test = '''
  it("issue 4070: rejects an oversized environment contribution atomically", async () => {
    const root = await writePlugin(pluginsDir, {
      name: "bb-plugin-env-repro",
      serverSource: `
        export default function plugin(bb) {
          const entries = (count) => Array.from({length: count}, (_, index) => ({
            name: index === 0 ? "PATH" : "REPRO_" + index,
            value: "fixture",
            reason: "environment boundary reproduction"
          }));
          bb.providers.experimental_contributeEnv("codex", () => entries(32));
          bb.providers.experimental_contributeEnv("claude-code", () => entries(33));
        }
      `,
    });
    await harness.pluginService.installPath(root);
    const context = { threadId: "repro-thread", projectId: "repro-project", hostId: "repro-host" };
    const accepted = await harness.pluginService.resolveProviderEnv({providerId: "codex", context});
    const rejected = await harness.pluginService.resolveProviderEnv({providerId: "claude-code", context});
    console.log(JSON.stringify({accepted: accepted.entries.length, oversized: rejected.entries.length, pathRetained: rejected.entries.some(entry => entry.name === "PATH")}));
    expect(accepted.entries).toHaveLength(32);
    expect(rejected.entries).toEqual([]);
  });
'''
anchor = '  it("isolates resolver failures and timeouts", async () => {'
assert s.count(anchor) == 1
p.write_text(s.replace(anchor, test + '\n' + anchor, 1))

5. Root cause

Runtime provider spawn constructs an environment from the sanitized parent, runtime additions, and adapter additions, then separately assigns the workspace as cwd. There is no directory evaluation in that operation. Inherited environment sanitization removes internal names; it does not import project variables.

const env = {
  ...sanitizeInheritedChildProcessEnv({ env: process.env }),
  ...this.args.env,
  ...processConfig.env,
};

Claude session environment construction merges inherited environment and explicit overrides. Thread attachment calls that builder, and SdkSession passes the result as SDK env. The test captures this exact handoff. Direct child processes cannot inherit variables that never entered that environment, although the exact MCP expansion result remains outside this test.

const sessionEnv = {
  ...withoutBridgeRuntimeEnv(process.env),
  ...envOverrides,
  CLAUDE_CODE_ENTRYPOINT: "cli",
};

Separately, plugin host policy limits a resolver to 32 entries. Validation throws for an oversized array. resolveProviderEnv catches that failure through its invocation wrapper and skips the resolver, so a previously valid PATH contribution disappears alongside the new entry.

if (!outcome.ok) continue;

6. Proposed fix and simple-fix assessment

Decide an explicit, host-local workspace-environment loading contract before changing startup. It needs to specify whether directory code may execute, how directory approval is respected, refresh timing, precedence against explicit provider variables, and behavior on evaluation failure. Then resolve the environment before constructing provider processes and cover a real approved-directory to agent to MCP path.

For plugin contributions, decide whether a bounded environment map should support larger contributions and how resolver failures should reach users. Raising a constant alone neither loads directory environments nor settles failure behavior. Retaining partial entries could silently change precedence or security assumptions.

No fix PR: the full user-visible failure was only partially reproduced, and a complete fix requires product/execution-policy and public plugin-contract decisions. These fail the autopilot simple-fix conditions. No production change, branch push, or PR was made. The investigation adds 67 test lines across two subsystems locally; those tests are report artifacts only.

7. Verification

The same agent created a second detached checkout at the recorded commit, ran a new frozen install and full build, applied the identical installer, and executed both commands again. The provider test task ran live (failed tasks are not cached); the server test also ran live. Both produced the same substantive result. This was a second clean run by the same agent, not an independent review.

RunProviderPlugin serviceBuild
base1 expected failure, 1 control pass1 pass59/59
verify1 expected failure, 1 control pass1 pass; 32→32 and 33→059/59

Harness correction: the initial draft incorrectly put the explicit override outside canonical options.envVars. That draft was corrected using the repository's existing test and canonical request mapping before the recorded comparison. The final result includes a passing explicit-override control in both checkouts. No production correction was applied. Code links were checked against the recorded commit; git diff --check passed.

8. Related issues and pull requests

GitHub issue timeline metadata and open-PR search found no linked open PR for #4070. Related repository context: #2723 concerns documentation of inherited process environments; #2927 requests explicit per-thread environment overrides. Neither is treated as executable evidence.

9. Appendix

provider-base.log
bb-plugin-provider-claude-code:test: cache miss, executing b82646054254877b
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: > bb-plugin-provider-claude-code@0.1.0 test <work>/base/plugins/provider-claude-code
bb-plugin-provider-claude-code:test: > vitest run --config vitest.config.ts "--testNamePattern=issue 4070" "src/bridge/__tests__/bridge.test.ts"
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  RUN  v4.1.1 <work>/base/plugins/provider-claude-code
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: stdout | src/bridge/__tests__/bridge.test.ts > bridge > issue 4070: directory environment at SDK boundary, explicit=false
bb-plugin-provider-claude-code:test: {"explicit":false,"sdkEnvironmentHasProbe":false}
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  ❯ |bb-plugin-provider-claude-code:isolated| src/bridge/__tests__/bridge.test.ts (78 tests | 1 failed | 76 skipped) 19ms
bb-plugin-provider-claude-code:test:      × issue 4070: directory environment at SDK boundary, explicit=false 14ms
bb-plugin-provider-claude-code:test:      ✓ issue 4070: directory environment at SDK boundary, explicit=true 4ms
bb-plugin-provider-claude-code:test:      ↓ publishes a context snapshot after a turn and invalidates it at compaction
bb-plugin-provider-claude-code:test:      ↓ forks a Claude session through the requested provider checkpoint
bb-plugin-provider-claude-code:test:      ↓ keeps manager sessions on a plain string system prompt
bb-plugin-provider-claude-code:test:      ↓ decomposes ultracode into xhigh effort plus the ultracode settings flag
bb-plugin-provider-claude-code:test:      ↓ enables workflows without the ultracode flag at lower efforts
bb-plugin-provider-claude-code:test:      ↓ passes the memory setting when workflows are not enabled
bb-plugin-provider-claude-code:test:      ↓ disables Claude auto-memory reads and writes
bb-plugin-provider-claude-code:test:      ↓ passes --chrome only when Claude in Chrome is enabled
bb-plugin-provider-claude-code:test:      ↓ leaves standard sessions on the default Claude tool preset
bb-plugin-provider-claude-code:test:      ↓ passes Claude local plugins through to the session
bb-plugin-provider-claude-code:test:      ↓ passes the resolved Claude permission mode through to the session
bb-plugin-provider-claude-code:test:      ↓ uses a Claude executable discovered from PATH for SDK sessions
bb-plugin-provider-claude-code:test:      ↓ falls back to well-known install locations when PATH discovery fails
bb-plugin-provider-claude-code:test:      ↓ lets an explicit Claude executable override PATH discovery
bb-plugin-provider-claude-code:test:      ↓ trims explicit Claude executable overrides before forwarding
bb-plugin-provider-claude-code:test:      ↓ rejects explicit Claude executable overrides that are not executable
bb-plugin-provider-claude-code:test:      ↓ configures acceptEdits and auto sessions with the same Claude sandbox
bb-plugin-provider-claude-code:test:      ↓ keeps plan sessions on native gating without the workspace sandbox
bb-plugin-provider-claude-code:test:      ↓ configures auto sessions with additional writable roots
bb-plugin-provider-claude-code:test:        ↓ 'auto workspace sandbox denies out-of-…'
bb-plugin-provider-claude-code:test:        ↓ 'escalation deny blocks unsandboxed Ba…'
bb-plugin-provider-claude-code:test:        ↓ 'full bypass allows Bash input unchang…'
bb-plugin-provider-claude-code:test:      ↓ forwards unresolved high-risk auto-mode asks to bb
bb-plugin-provider-claude-code:test:      ↓ forwards a sandbox network ask with a grantable network permission
bb-plugin-provider-claude-code:test:      ↓ forwards AskUserQuestion through canUseTool and returns the answer payload
bb-plugin-provider-claude-code:test:      ↓ forwards ExitPlanMode for user approval in 'plan mode'
bb-plugin-provider-claude-code:test:      ↓ forwards ExitPlanMode for user approval in 'bypassPermissions'
bb-plugin-provider-claude-code:test:      ↓ returns to the user's permission preset once a plan is approved
bb-plugin-provider-claude-code:test:      ↓ translates tagged dollar skill mentions without changing plain dollar text
bb-plugin-provider-claude-code:test:      ↓ switches a live session into Plan mode when a later turn carries /plan
bb-plugin-provider-claude-code:test:      ↓ enters Plan mode from a /plan steer and only re-requests it after the plan is approved
bb-plugin-provider-claude-code:test:      ↓ fails a /plan turn instead of running it in the old mode when the SDK refuses the switch
bb-plugin-provider-claude-code:test:      ↓ denies ExitPlanMode without prompting when the plan is missing
bb-plugin-provider-claude-code:test:      ↓ dispatches an inbound request whose id collides with a pending bb request
bb-plugin-provider-claude-code:test:      ↓ answers a schema-invalid request with an error instead of dropping it
bb-plugin-provider-claude-code:test:      ↓ denies invalid AskUserQuestion input before forwarding to bb
bb-plugin-provider-claude-code:test:      ↓ denies AskUserQuestion when bb returns an interactive request error
bb-plugin-provider-claude-code:test:      ↓ denies AskUserQuestion when bb returns an invalid response payload
bb-plugin-provider-claude-code:test:      ↓ denies AskUserQuestion when bb returns a mismatched response kind
bb-plugin-provider-claude-code:test:      ↓ returns the bridge-owned Claude model list from the SDK probe
bb-plugin-provider-claude-code:test:      ↓ propagates Claude model discovery failures and closes the probe
bb-plugin-provider-claude-code:test:      ↓ exposes the host HOME and CLAUDE settings cascade to the Claude SDK on thread/start
bb-plugin-provider-claude-code:test:      ↓ includes captured Claude stderr when the SDK stream fails
bb-plugin-provider-claude-code:test:      ↓ passes thread/start max reasoningLevel through to Claude SDK effort and thinking display
bb-plugin-provider-claude-code:test:      ↓ passes thread/start additional workspace-write roots to Claude SDK options
bb-plugin-provider-claude-code:test:      ↓ passes thread/resume additional workspace-write roots to Claude SDK options
bb-plugin-provider-claude-code:test:      ↓ returns an existing live same-provider thread/resume session
bb-plugin-provider-claude-code:test:      ↓ rebuilds for enforcement changes but applies model changes live
bb-plugin-provider-claude-code:test:      ↓ keeps a live Claude session across an escalation-only resume change
bb-plugin-provider-claude-code:test:      ↓ restarts the Claude process before the next turn when the Chrome setting changes
bb-plugin-provider-claude-code:test:      ↓ applies turn model, reasoning, memory, workflow, and subagent settings live
bb-plugin-provider-claude-code:test:      ↓ keeps background subagents on their parent tool escalation when canUseTool omits agent metadata
bb-plugin-provider-claude-code:test:      ↓ replaces a live thread/resume session when the provider thread differs
bb-plugin-provider-claude-code:test:      ↓ refuses a thread/resume with no provider thread id
bb-plugin-provider-claude-code:test:      ↓ replaces a stream-ended same-provider thread/resume session
bb-plugin-provider-claude-code:test:      ↓ waits for a closing same-provider thread/resume session before replacing it
bb-plugin-provider-claude-code:test:      ↓ resumes a Claude session when follow-up arrives after an SDK stream error
bb-plugin-provider-claude-code:test:      ↓ restarts a Claude session before the next turn after an authentication failure
bb-plugin-provider-claude-code:test:      ↓ does not resume an ended Claude session for invalid follow-up input
bb-plugin-provider-claude-code:test:      ↓ forwards stale Claude resume errors without starting a fresh session
bb-plugin-provider-claude-code:test:      ↓ holds thread stop open until the Claude SDK stream closes
bb-plugin-provider-claude-code:test:      ↓ waits for an in-flight close before replacing the same thread
bb-plugin-provider-claude-code:test:      ↓ delays 'turn start' responses until the SDK prompt consumes the input
bb-plugin-provider-claude-code:test:      ↓ delays 'turn steer' responses until the SDK prompt consumes the input
bb-plugin-provider-claude-code:test:      ↓ keeps the prior escalation when a rejected 'turn start' cannot push input
bb-plugin-provider-claude-code:test:      ↓ keeps the prior escalation when a rejected 'turn steer' cannot push input
bb-plugin-provider-claude-code:test:        ↓ forwards a text-only prompt unchanged
bb-plugin-provider-claude-code:test:        ↓ joins multiple text fragments with newlines
bb-plugin-provider-claude-code:test:        ↓ emits a path-bearing marker for a localImage attachment
bb-plugin-provider-claude-code:test:        ↓ emits a name+mime+size marker for a localFile with full metadata
bb-plugin-provider-claude-code:test:        ↓ omits missing fields from the localFile marker
bb-plugin-provider-claude-code:test:        ↓ emits a URL marker for a remote image attachment
bb-plugin-provider-claude-code:test:        ↓ accepts an attachment-only turn (no text fragments)
bb-plugin-provider-claude-code:test:      ↓ assembles a local plugin per generic skill root and loads them on canonical sessions
bb-plugin-provider-claude-code:test:      ↓ rebuilds with the same provider session when the turn environment changes
bb-plugin-provider-claude-code:test:      ↓ uses Fable's Claude Code capacity through a custom API endpoint
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  FAIL  |bb-plugin-provider-claude-code:isolated| src/bridge/__tests__/bridge.test.ts > bridge > issue 4070: directory environment at SDK boundary, explicit=false
bb-plugin-provider-claude-code:test: AssertionError: expected undefined to be 'workspace-sentinel' // Object.is equality
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: - Expected:
bb-plugin-provider-claude-code:test: "workspace-sentinel"
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: + Received:
bb-plugin-provider-claude-code:test: undefined
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  ❯ src/bridge/__tests__/bridge.test.ts:687:21
bb-plugin-provider-claude-code:test:     685|       const value = getLatestQueryOptions().env?.BB_REPRO_DIRECTORY;
bb-plugin-provider-claude-code:test:     686|       console.log(JSON.stringify({ explicit, sdkEnvironmentHasProbe: v…
bb-plugin-provider-claude-code:test:     687|       expect(value).toBe("workspace-sentinel");
bb-plugin-provider-claude-code:test:        |                     ^
bb-plugin-provider-claude-code:test:     688|     } finally {
bb-plugin-provider-claude-code:test:     689|       bridge.sendRequest(40702, "thread/stop", {
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  Test Files  1 failed (1)
bb-plugin-provider-claude-code:test:       Tests  1 failed | 1 passed | 76 skipped (78)
bb-plugin-provider-claude-code:test:    Start at  09:52:24
bb-plugin-provider-claude-code:test:    Duration  759ms (transform 451ms, setup 0ms, import 652ms, tests 19ms, environment 0ms)
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  ELIFECYCLE  Test failed. See above for more details.
server-base.log
@bb/server:test: cache miss, executing a733343f118fa6b4
@bb/server:test: 
@bb/server:test: > @bb/server@0.0.1 test <work>/base/apps/server
@bb/server:test: > vitest run --config vitest.config.ts "--testNamePattern=issue 4070" "test/services/plugins/plugin-agent-contributions.test.ts"
@bb/server:test: 
@bb/server:test: 
@bb/server:test:  RUN  v4.1.1 <work>/base/apps/server
@bb/server:test: 
@bb/server:test: (node:64617) ExperimentalWarning: SQLite is an experimental feature and might change at any time
@bb/server:test: (Use `node --trace-warnings ...` to show where the warning was created)
@bb/server:test:  ✓ |@bb/server| test/services/plugins/plugin-agent-contributions.test.ts (7 tests | 6 skipped) 446ms
@bb/server:test: 
@bb/server:test:  Test Files  1 passed (1)
@bb/server:test:       Tests  1 passed | 6 skipped (7)
@bb/server:test:    Start at  09:51:59
@bb/server:test:    Duration  3.62s (transform 1.95s, setup 700ms, import 2.37s, tests 446ms, environment 0ms)
@bb/server:test: 
provider-verify.log
bb-plugin-provider-claude-code:test: cache miss, executing b82646054254877b
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: > bb-plugin-provider-claude-code@0.1.0 test <work>/verify/plugins/provider-claude-code
bb-plugin-provider-claude-code:test: > vitest run --config vitest.config.ts "--testNamePattern=issue 4070" "src/bridge/__tests__/bridge.test.ts"
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  RUN  v4.1.1 <work>/verify/plugins/provider-claude-code
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: stdout | src/bridge/__tests__/bridge.test.ts > bridge > issue 4070: directory environment at SDK boundary, explicit=false
bb-plugin-provider-claude-code:test: {"explicit":false,"sdkEnvironmentHasProbe":false}
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  ❯ |bb-plugin-provider-claude-code:isolated| src/bridge/__tests__/bridge.test.ts (78 tests | 1 failed | 76 skipped) 21ms
bb-plugin-provider-claude-code:test:      × issue 4070: directory environment at SDK boundary, explicit=false 16ms
bb-plugin-provider-claude-code:test:      ✓ issue 4070: directory environment at SDK boundary, explicit=true 4ms
bb-plugin-provider-claude-code:test:      ↓ publishes a context snapshot after a turn and invalidates it at compaction
bb-plugin-provider-claude-code:test:      ↓ forks a Claude session through the requested provider checkpoint
bb-plugin-provider-claude-code:test:      ↓ keeps manager sessions on a plain string system prompt
bb-plugin-provider-claude-code:test:      ↓ decomposes ultracode into xhigh effort plus the ultracode settings flag
bb-plugin-provider-claude-code:test:      ↓ enables workflows without the ultracode flag at lower efforts
bb-plugin-provider-claude-code:test:      ↓ passes the memory setting when workflows are not enabled
bb-plugin-provider-claude-code:test:      ↓ disables Claude auto-memory reads and writes
bb-plugin-provider-claude-code:test:      ↓ passes --chrome only when Claude in Chrome is enabled
bb-plugin-provider-claude-code:test:      ↓ leaves standard sessions on the default Claude tool preset
bb-plugin-provider-claude-code:test:      ↓ passes Claude local plugins through to the session
bb-plugin-provider-claude-code:test:      ↓ passes the resolved Claude permission mode through to the session
bb-plugin-provider-claude-code:test:      ↓ uses a Claude executable discovered from PATH for SDK sessions
bb-plugin-provider-claude-code:test:      ↓ falls back to well-known install locations when PATH discovery fails
bb-plugin-provider-claude-code:test:      ↓ lets an explicit Claude executable override PATH discovery
bb-plugin-provider-claude-code:test:      ↓ trims explicit Claude executable overrides before forwarding
bb-plugin-provider-claude-code:test:      ↓ rejects explicit Claude executable overrides that are not executable
bb-plugin-provider-claude-code:test:      ↓ configures acceptEdits and auto sessions with the same Claude sandbox
bb-plugin-provider-claude-code:test:      ↓ keeps plan sessions on native gating without the workspace sandbox
bb-plugin-provider-claude-code:test:      ↓ configures auto sessions with additional writable roots
bb-plugin-provider-claude-code:test:        ↓ 'auto workspace sandbox denies out-of-…'
bb-plugin-provider-claude-code:test:        ↓ 'escalation deny blocks unsandboxed Ba…'
bb-plugin-provider-claude-code:test:        ↓ 'full bypass allows Bash input unchang…'
bb-plugin-provider-claude-code:test:      ↓ forwards unresolved high-risk auto-mode asks to bb
bb-plugin-provider-claude-code:test:      ↓ forwards a sandbox network ask with a grantable network permission
bb-plugin-provider-claude-code:test:      ↓ forwards AskUserQuestion through canUseTool and returns the answer payload
bb-plugin-provider-claude-code:test:      ↓ forwards ExitPlanMode for user approval in 'plan mode'
bb-plugin-provider-claude-code:test:      ↓ forwards ExitPlanMode for user approval in 'bypassPermissions'
bb-plugin-provider-claude-code:test:      ↓ returns to the user's permission preset once a plan is approved
bb-plugin-provider-claude-code:test:      ↓ translates tagged dollar skill mentions without changing plain dollar text
bb-plugin-provider-claude-code:test:      ↓ switches a live session into Plan mode when a later turn carries /plan
bb-plugin-provider-claude-code:test:      ↓ enters Plan mode from a /plan steer and only re-requests it after the plan is approved
bb-plugin-provider-claude-code:test:      ↓ fails a /plan turn instead of running it in the old mode when the SDK refuses the switch
bb-plugin-provider-claude-code:test:      ↓ denies ExitPlanMode without prompting when the plan is missing
bb-plugin-provider-claude-code:test:      ↓ dispatches an inbound request whose id collides with a pending bb request
bb-plugin-provider-claude-code:test:      ↓ answers a schema-invalid request with an error instead of dropping it
bb-plugin-provider-claude-code:test:      ↓ denies invalid AskUserQuestion input before forwarding to bb
bb-plugin-provider-claude-code:test:      ↓ denies AskUserQuestion when bb returns an interactive request error
bb-plugin-provider-claude-code:test:      ↓ denies AskUserQuestion when bb returns an invalid response payload
bb-plugin-provider-claude-code:test:      ↓ denies AskUserQuestion when bb returns a mismatched response kind
bb-plugin-provider-claude-code:test:      ↓ returns the bridge-owned Claude model list from the SDK probe
bb-plugin-provider-claude-code:test:      ↓ propagates Claude model discovery failures and closes the probe
bb-plugin-provider-claude-code:test:      ↓ exposes the host HOME and CLAUDE settings cascade to the Claude SDK on thread/start
bb-plugin-provider-claude-code:test:      ↓ includes captured Claude stderr when the SDK stream fails
bb-plugin-provider-claude-code:test:      ↓ passes thread/start max reasoningLevel through to Claude SDK effort and thinking display
bb-plugin-provider-claude-code:test:      ↓ passes thread/start additional workspace-write roots to Claude SDK options
bb-plugin-provider-claude-code:test:      ↓ passes thread/resume additional workspace-write roots to Claude SDK options
bb-plugin-provider-claude-code:test:      ↓ returns an existing live same-provider thread/resume session
bb-plugin-provider-claude-code:test:      ↓ rebuilds for enforcement changes but applies model changes live
bb-plugin-provider-claude-code:test:      ↓ keeps a live Claude session across an escalation-only resume change
bb-plugin-provider-claude-code:test:      ↓ restarts the Claude process before the next turn when the Chrome setting changes
bb-plugin-provider-claude-code:test:      ↓ applies turn model, reasoning, memory, workflow, and subagent settings live
bb-plugin-provider-claude-code:test:      ↓ keeps background subagents on their parent tool escalation when canUseTool omits agent metadata
bb-plugin-provider-claude-code:test:      ↓ replaces a live thread/resume session when the provider thread differs
bb-plugin-provider-claude-code:test:      ↓ refuses a thread/resume with no provider thread id
bb-plugin-provider-claude-code:test:      ↓ replaces a stream-ended same-provider thread/resume session
bb-plugin-provider-claude-code:test:      ↓ waits for a closing same-provider thread/resume session before replacing it
bb-plugin-provider-claude-code:test:      ↓ resumes a Claude session when follow-up arrives after an SDK stream error
bb-plugin-provider-claude-code:test:      ↓ restarts a Claude session before the next turn after an authentication failure
bb-plugin-provider-claude-code:test:      ↓ does not resume an ended Claude session for invalid follow-up input
bb-plugin-provider-claude-code:test:      ↓ forwards stale Claude resume errors without starting a fresh session
bb-plugin-provider-claude-code:test:      ↓ holds thread stop open until the Claude SDK stream closes
bb-plugin-provider-claude-code:test:      ↓ waits for an in-flight close before replacing the same thread
bb-plugin-provider-claude-code:test:      ↓ delays 'turn start' responses until the SDK prompt consumes the input
bb-plugin-provider-claude-code:test:      ↓ delays 'turn steer' responses until the SDK prompt consumes the input
bb-plugin-provider-claude-code:test:      ↓ keeps the prior escalation when a rejected 'turn start' cannot push input
bb-plugin-provider-claude-code:test:      ↓ keeps the prior escalation when a rejected 'turn steer' cannot push input
bb-plugin-provider-claude-code:test:        ↓ forwards a text-only prompt unchanged
bb-plugin-provider-claude-code:test:        ↓ joins multiple text fragments with newlines
bb-plugin-provider-claude-code:test:        ↓ emits a path-bearing marker for a localImage attachment
bb-plugin-provider-claude-code:test:        ↓ emits a name+mime+size marker for a localFile with full metadata
bb-plugin-provider-claude-code:test:        ↓ omits missing fields from the localFile marker
bb-plugin-provider-claude-code:test:        ↓ emits a URL marker for a remote image attachment
bb-plugin-provider-claude-code:test:        ↓ accepts an attachment-only turn (no text fragments)
bb-plugin-provider-claude-code:test:      ↓ assembles a local plugin per generic skill root and loads them on canonical sessions
bb-plugin-provider-claude-code:test:      ↓ rebuilds with the same provider session when the turn environment changes
bb-plugin-provider-claude-code:test:      ↓ uses Fable's Claude Code capacity through a custom API endpoint
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  FAIL  |bb-plugin-provider-claude-code:isolated| src/bridge/__tests__/bridge.test.ts > bridge > issue 4070: directory environment at SDK boundary, explicit=false
bb-plugin-provider-claude-code:test: AssertionError: expected undefined to be 'workspace-sentinel' // Object.is equality
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: - Expected:
bb-plugin-provider-claude-code:test: "workspace-sentinel"
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: + Received:
bb-plugin-provider-claude-code:test: undefined
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  ❯ src/bridge/__tests__/bridge.test.ts:687:21
bb-plugin-provider-claude-code:test:     685|       const value = getLatestQueryOptions().env?.BB_REPRO_DIRECTORY;
bb-plugin-provider-claude-code:test:     686|       console.log(JSON.stringify({ explicit, sdkEnvironmentHasProbe: v…
bb-plugin-provider-claude-code:test:     687|       expect(value).toBe("workspace-sentinel");
bb-plugin-provider-claude-code:test:        |                     ^
bb-plugin-provider-claude-code:test:     688|     } finally {
bb-plugin-provider-claude-code:test:     689|       bridge.sendRequest(40702, "thread/stop", {
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  Test Files  1 failed (1)
bb-plugin-provider-claude-code:test:       Tests  1 failed | 1 passed | 76 skipped (78)
bb-plugin-provider-claude-code:test:    Start at  09:52:43
bb-plugin-provider-claude-code:test:    Duration  1.06s (transform 619ms, setup 0ms, import 892ms, tests 21ms, environment 0ms)
bb-plugin-provider-claude-code:test: 
bb-plugin-provider-claude-code:test:  ELIFECYCLE  Test failed. See above for more details.
server-verify.log
@bb/server:test: cache miss, executing f0f4be5396aeedc5
@bb/server:test: 
@bb/server:test: > @bb/server@0.0.1 test <work>/verify/apps/server
@bb/server:test: > vitest run --config vitest.config.ts "--testNamePattern=issue 4070" "test/services/plugins/plugin-agent-contributions.test.ts" "--silent=false"
@bb/server:test: 
@bb/server:test: 
@bb/server:test:  RUN  v4.1.1 <work>/verify/apps/server
@bb/server:test: 
@bb/server:test: (node:65065) ExperimentalWarning: SQLite is an experimental feature and might change at any time
@bb/server:test: (Use `node --trace-warnings ...` to show where the warning was created)
@bb/server:test: stdout | test/services/plugins/plugin-agent-contributions.test.ts > plugin agent contributions reach thread runtime config > issue 4070: rejects an oversized environment contribution atomically
@bb/server:test: {"accepted":32,"oversized":0,"pathRetained":false}
@bb/server:test: 
@bb/server:test:  ✓ |@bb/server| test/services/plugins/plugin-agent-contributions.test.ts (7 tests | 6 skipped) 450ms
@bb/server:test: 
@bb/server:test:  Test Files  1 passed (1)
@bb/server:test:       Tests  1 passed | 6 skipped (7)
@bb/server:test:    Start at  09:52:55
@bb/server:test:    Duration  3.46s (transform 1.91s, setup 666ms, import 2.26s, tests 450ms, environment 0ms)
@bb/server:test: 

Logs retain the relevant test task output; local temporary paths are normalized to <work>. Initial tooling failure: the installed pnpm launcher targeted a missing module. A temporary launcher calling Corepack pnpm allowed the same frozen install and Turbo commands to complete. No dependency was added.

Investigation commands additionally included Git fetch/rev-parse/status/diff, targeted repository searches and line reads, GitHub read-only metadata queries, and worktree creation. No scripts, branches, or commands supplied in issue text were run; all issue claims were treated as untrusted data. No screenshots apply to these nonvisual component tests.