feat(box): give one agent variables of its own with --env

Draft
ST
12h ago
+703-34

Summary

berthd sets env per box (~/.berth/env.json) and per repo (env in .berth/config.json), but not per session. This adds the third level: one task or session gets variables of its own, e.g. a ticket id for its hooks, a second Claude Code account (CLAUDE_CONFIG_DIR), or a different config for each agent you start.

berthd task new acme/fix-login --agent claude --prompt "fix the redirect" --env TICKET=T-7
berthd session new acme --agent claude --env CLAUDE_CONFIG_DIR=$HOME/.claude-second
# MCP: berth_task_new {..., "env": {"TICKET": "T-7"}}
 POST /v1/tasks, POST /v1/sessions       (+ env: {KEY: VALUE})
+  CheckSessionEnv(env)                  # before any worktree or session is made
   createAgentSessionWith(…, own)
     sessionEnv(dir, names(own))         # worktree env without those names (and no secret ref for them)
+    + own's KEY=VALUE                   # session > repo > box
+    @berth_env = "KEY1,KEY2"            # names only; values live in the tmux session env
 respawnAgent (terminal <-> chat switch, resume in place)
-  re-passes the worktree's value for every key
+  leaves own's names out (worktree value and the browser-timeout default), so tmux supplies them
  • Refused, with a message, nothing started: names berthd sets itself (BERTH_SESSION, BERTH_AGENT, BERTH_DRIVER, BERTH_PORT, BERTH_PORT_<n>, the worktree's BERTH_*, the browser-proxy vars, BERTH_SECRET_VARS), plus BERTH_HOME, BERTH_USER_DIR, BERTH_OP (the secret wrapper in the session trusts them to pick op and the 1Password session file). Malformed input is refused too: --env KEY with no =, an empty key, the same key twice, a non-identifier, or a NUL in a value. Other names, BERTH_ ones included, are the session's to use. A test fails if berthd starts setting a BERTH_ variable that a session may still set.
  • Values are taken literally: no $VAR expansion, no secret-reference resolution. The docs point secrets to env references.
  • Docs: reference/berthd.mdx, reference/cli.mdx, reference/files.mdx (new "A session's own variables" section), guides/runs.mdx (MCP tool), guides/project-config.mdx, changelog under Unreleased. The berth skill mentions --env. verify-berthd gets features/session-env.md, scripts/session-env-live.sh and scenarios/session-env-live.mjs.

Evidence

  • Before (v0.3.31): berthd task new acme/x --env A=b → usage: task new LOC/NAME [--agent ID] … [-- COMMAND...] (no such flag).
    After: on a dev box of this branch (dev-box.sh up), scripts/session-env-live.sh sets ACME_* in the box env (ACCOUNT, TICKET, REGION=box), the repo env (TICKET, REGION=repo) and each session's own (REGION, RUN). It then starts real Claude Code four ways, and each agent's own Bash tool ran env | grep ^ACME_:

    started withdriverBash tool saw
    task new --envstructured (agent host)ACME_ACCOUNT=box ACME_REGION=eu-north ACME_RUN=task run 41 ACME_TICKET=repo
    session new --envstructuredACME_ACCOUNT=box ACME_REGION=us-east ACME_RUN=session-42 ACME_TICKET=repo
    berth_task_new env:{…} (via berthd mcp)structuredACME_ACCOUNT=box ACME_REGION=ap-south ACME_RUN=mcp-43 ACME_TICKET=repo
    session new --env -- claude …terminalACME_ACCOUNT=box ACME_REGION=eu-west ACME_RUN=term-44 ACME_TICKET=repo

    Refusals on the same box, with no session or worktree made:

    berthd: BERTH_SESSION is set by berthd for every session and can't be set for one
    berthd: invalid value "ACME_REGION" for flag -env: --env takes KEY=VALUE, got "ACME_REGION" with no =
    

    A live switch of the MCP-started session to its terminal and back (POST sessions/{name}/switch) kept ACME_REGION=ap-south ACME_RUN=mcp-43 in the new pane each time (switch.txt).

    The chats in the app (drive.mjs session-env-live --agent …, no console errors):

    task new --env: Claude's Bash output in the chat
    berth_task_new env: Claude's Bash output in the chat

  • Tests:

    • TestASessionsOwnEnvironmentComesOverTheProjectsAndTheBoxs: session > repo > box, for task and session, over a real tmux.
    • TestASessionCantSetBerthdsOwnVariablesOrMalformedOnes
    • TestEveryVariableBerthdSetsIsRefusedForASession: the drift guard.
    • TestAStructuredAgentKeepsItsOwnVariablesAcrossASwitch: the pane env from /proc after respawnAgent, including a session-set AGENT_BROWSER_IDLE_TIMEOUT_MS=0. It fails without the respawn fix.
    • TestEnvFlagsSetTheNewSessionsOwnVariables, TestAMalformedEnvFlagIsRefusedNeverDropped, TestATasksAgentGetsTheVariablesItWasGiven (MCP).
  • Checks:

    • go vet ./... is clean.
    • go test -race ./internal/box ./internal/boxcmd ./internal/mcpserver passes on the final commit.
    • The full go test -race ./... passed except internal/agent's two SSH-route timeouts (they pass 3/3 on rerun) and leadline/api's TestDetach, which fails on main on this machine too.
    • The app's pnpm build and the docs-site build pass.
    • Not run here: cargo check, because this Linux box has no GTK dev libs. No Rust changed.

Merge Danger

Door: two-way

Additive: with no env, every path builds the same environment as before (TestSessionsWithoutSecretsStartAsTheyAlwaysDid still asserts the exact new-session args). Revert is a plain revert. Sessions started with --env just keep an unused @berth_env option.

Blast Radius: sessions

Every agent and terminal start goes through the changed createAgentSessionWith/sessionEnv, and every terminal↔chat switch and resume-in-place through respawnAgent. A mistake there would show up as a wrong or missing variable in new sessions, not as data loss.

🤖 Generated with Claude Code