feat(box): give one agent variables of its own with --env
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'sBERTH_*, the browser-proxy vars,BERTH_SECRET_VARS), plusBERTH_HOME,BERTH_USER_DIR,BERTH_OP(the secret wrapper in the session trusts them to pickopand the 1Password session file). Malformed input is refused too:--env KEYwith 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 aBERTH_variable that a session may still set. - Values are taken literally: no
$VARexpansion, no secret-reference resolution. The docs point secrets toenvreferences. - 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. Theberthskill mentions--env. verify-berthd getsfeatures/session-env.md,scripts/session-env-live.shandscenarios/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.shsetsACME_*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 ranenv | grep ^ACME_:started with driver Bash tool saw task new --envstructured (agent host) ACME_ACCOUNT=box ACME_REGION=eu-north ACME_RUN=task run 41 ACME_TICKET=reposession new --envstructured ACME_ACCOUNT=box ACME_REGION=us-east ACME_RUN=session-42 ACME_TICKET=repoberth_task_newenv:{…}(viaberthd mcp)structured ACME_ACCOUNT=box ACME_REGION=ap-south ACME_RUN=mcp-43 ACME_TICKET=reposession new --env -- claude …terminal ACME_ACCOUNT=box ACME_REGION=eu-west ACME_RUN=term-44 ACME_TICKET=repoRefusals 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) keptACME_REGION=ap-south ACME_RUN=mcp-43in the new pane each time (switch.txt).The chats in the app (
drive.mjs session-env-live --agent …, no console errors):

-
Tests:
TestASessionsOwnEnvironmentComesOverTheProjectsAndTheBoxs: session > repo > box, for task and session, over a real tmux.TestASessionCantSetBerthdsOwnVariablesOrMalformedOnesTestEveryVariableBerthdSetsIsRefusedForASession: the drift guard.TestAStructuredAgentKeepsItsOwnVariablesAcrossASwitch: the pane env from/procafterrespawnAgent, including a session-setAGENT_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/mcpserverpasses on the final commit.- The full
go test -race ./...passed exceptinternal/agent's two SSH-route timeouts (they pass 3/3 on rerun) andleadline/api'sTestDetach, which fails on main on this machine too. - The app's
pnpm buildand 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