Foreman›Docs›Making it yours›Story sessions

Story sessions

Making it yours · page 2 of 4

What a story session loads, may use and may not do: tools, MCP servers, your user settings, checks and searches.

Foreman 0.11.08 sections

A story session is the Claude Code process the supervisor starts for one story in one slot (a slot is a git worktree of your project with its own environment). It runs claude -p "/foreman:story <id>" in the slot’s worktree, the story skill drives the agents, and the session ends when the story has shipped, stopped or failed. This page says what such a session loads, which tools and servers it may use, and the rules Foreman’s hooks enforce on every agent in it. Read it when you want to give sessions an MCP server or a tool, when a command an agent ran was denied, or when you wonder what a session can reach on your machine.

Lean by default

A story session loads Foreman, the plugins the project’s pack needs and the MCP servers you name, and nothing else from your own Claude Code set-up. Foreman starts it with:

  • --setting-sources project,local: the project’s .claude/settings.json and .claude/settings.local.json are read; your user-level settings, with their hooks, plugins, skills, agents and MCP servers, are not.
  • --strict-mcp-config: no MCP server starts unless it is in the session’s own MCP file (below).
  • --settings <file> with claudeMdExcludes set to your user-level CLAUDE.md (under CLAUDE_CONFIG_DIR, else ~/.claude), its imports included, and autoMemoryEnabled false. So a session reads the project’s CLAUDE.md, not yours, and no auto memory.
  • --disallowedTools ScheduleWakeup CronCreate: a wake-up still scheduled when the session prints its result would keep claude -p alive for 30 to 60 minutes more, and the pipeline’s own completion wakes the session anyway. A deny wins over an allow, so session.extraTools cannot bring them back.
  • --allowedTools with exactly Bash, Read, Edit, Write, Glob, Grep, Agent, Skill, ToolSearch and Workflow(foreman:pipeline), plus ArtifactData when the console adapter is artifacts, plus session.extraTools. Anything else is denied.
  • --output-format stream-json --verbose --permission-prompts none, so the session never waits for a prompt and its log carries the cost, turns and time of the run.

Some of your user-level settings are kept, because they decide how a session runs and add no context: model, effortLevel, modelSettings, alwaysThinkingEnabled, attribution, includeCoAuthoredBy, env, permissions, sandbox, apiKeyHelper, forceLoginMethod, forceLoginOrgUUID, awsAuthRefresh, awsCredentialExport and skipDangerousModePermissionPrompt. Each is carried only where the project’s settings do not set it (for an object such as permissions or env, sub-key by sub-key; a list such as permissions.allow is always carried, because Claude Code joins lists across layers). Your sign-in is unchanged.

Plugins: every plugin the project’s settings enable is set to off in the session, except Foreman and the plugins the pack’s session needs name, which are on. A Foreman enabled only in your user settings is turned on too, so it still loads.

The settings and the MCP servers reach the session as files, never on the command line, because the settings can hold your env and apiKeyHelper and the MCP config the servers’ keys, and anyone on the machine can read a process’s arguments. The files are <state folder>/session-config/<slot>-settings.json and <slot>-mcp.json, written with mode 600 before the session starts, one pair per slot; the next session of that slot writes over them. A third file, <slot>-env.json, holds the session’s whole environment, for foreman checks (below). When one of the files cannot be written, no session starts. The session’s transcript goes to <state folder>/logs/<slot>-<time>.log (-2, -3 when two sessions of the slot start in the same second).

foreman report shows each day’s average first-turn context size, so you can see what a session starts with (Costs and reports). foreman doctor (‘story session scope’) says what sessions leave out and checks the two settings below.

session.extraTools

session.extraTools, a list of tool names such as ["mcp__acme__query"], adds tools that story sessions may use. Each name is one argument to claude, so it must have no spaces and must not start with -; config validation refuses anything else, and a pack default that fails that rule is dropped silently. A pack may give the list a default, and a list in foreman.config.json replaces the pack’s list whole. It is not a setting: the local settings file and the dashboard cannot change it.

Tools of an MCP server still need that server in session.mcpServers, or the session has the name and no server behind it. foreman doctor warns when an entry mcp__<server>__... names a server session.mcpServers does not. ScheduleWakeup and CronCreate stay denied whatever the list says.

session.mcpServers

session.mcpServers, such as ["acmeDb"], names the servers from the project’s .mcp.json that story sessions load. The default is none. A name is a letter or digit first, then letters, digits, _, . and -. The session’s MCP file holds the pack’s own servers first, then the ones you name, as .mcp.json defines them; a server you name that the pack also defines takes your definition.

The scope is worked out once when foreman run starts. A name that .mcp.json does not define, or a .mcp.json that cannot be read, stops the run before any session starts, and foreman doctor reports the same as an error, adding “defined only at user level in ~/.claude.json; story sessions do not load it” when that is where the server lives. Add the server to the project’s .mcp.json to fix it.

session.userSettings

session.userSettings (default false) decides whether story sessions read your user-level Claude Code settings. Off, they leave out your user-level CLAUDE.md and its imports, auto memory, hooks, skills, agents, plugins and MCP servers, and keep only the settings listed under Lean by default.

Set it to true to load your user settings, hooks, CLAUDE.md and memory too: Foreman then drops --setting-sources and the claudeMdExcludes and autoMemoryEnabled keys, and does not copy the carried settings (the session reads them itself). Plugins other than Foreman and the pack’s still turn off, and MCP servers still come only from session.mcpServers, since --strict-mcp-config stays either way. With it on, foreman doctor also looks for a project agent in ~/.claude/agents/ when it is not in the project’s .claude/agents/.

Checks run only through foreman checks

The project’s checks (the checks list in foreman.config.json) are the proof that a change works, and only foreman checks records a run. So in a story session Foreman denies a check’s command run by hand, from any agent, the story session itself included, and no setting switches that off.

What is denied: a Bash command one of whose simple commands starts with the words of a configured check’s run (its program and arguments, cd and foreman parts left out) and has only options (words starting with -) after them. Variables, NAME=value prefixes, redirects, pipes, a timeout wrapper, env, nohup and the other wrappers, and a script given to sh -c, bash -c, zsh -c or eval (three levels deep) do not get round it. What stays free: a positional word after the check’s words, so pytest tests/x.py or npm test -- file is a targeted run; a command that is not a configured check; and your own terminal, because the guard acts only inside a session (FOREMAN_RUN=1). The denial says what to do instead and names the check. It is also recorded as a denied: true line in <state folder>/check-runs.jsonl, and foreman report counts the denied hand runs per story. The agents’ prompts say the same, so a denial is rare.

The flags the agents use:

Flag What it does
(none) Runs the checks whose when is always.
--changed Those, plus each check whose when glob matches a file this branch changed against the base, an untracked file or an uncommitted change. When the base cannot be diffed, every check runs.
--all Every configured check, whatever its when; the story session uses it after a failed merge of the base. Not with --changed.
--reuse For each chosen check with a record of a real full run on this exact tree, prints that record (when, for which story, how long, the last lines, PASS or FAIL) and counts its exit code instead of running it. A check with no record runs.
--quick Runs each chosen check’s quick command where it has one, else its run. A quick run is recorded as quick, never reused, takes no claim and waits for nothing. Not with --reuse.
--recorded Prints the record of this tree for each chosen check and runs nothing; exit 1 only for a recorded failure. Not with --reuse, --quick, --start or --wait.
--start Starts the run detached, in its own process group with its output in files, and prints a run id at once.
--wait <id> [--for <seconds>] Polls the run for up to --for seconds (default 90). Finished: prints what a foreground run prints and exits with its code. Still running: “still running”, exit 75, so --wait && git commit can never go ahead early. Its process gone with no result: FAIL, exit 1.

A check is matched on its command and the tree, not on the folder it ran in, so --reuse and --recorded assume the repository root, where Foreman’s agents run them.

foreman checks in a story session runs each check in the session’s own environment, read from <state folder>/session-config/<slot>-env.json, not in whatever the calling shell has set: PATH, NODE_OPTIONS or a key an agent set does not change the result. Such a run is recorded with env session, and only those records are ever reused. When the file cannot be used, or the slot name is not one the supervisor makes (w1 to w<slots>), the checks run in the caller’s environment with a warning, are recorded as caller and are never reused. Outside a session that is the plain case: caller environment, no warning.

So the newest full run on a tree, normally the tester’s, is the one the reviewers (--changed --reuse) and the commit step print when nothing changed since, and anything changed after it, such as a fix round, runs the checks again: a story is never committed without a passing full-suite result for its exact tree. The release’s check of the merged code reuses a record of that same merged tree. Before a real run in the session environment a check takes a claim on its command and tree; when two slots reach the same check on the same tree at once, the second says it is waiting, prints the first one’s result when it ends and records a waited reuse. A claim whose process is gone is dropped and the check runs; a claim that cannot be made is a warning and the check runs unclaimed.

A check may carry quick, the project’s faster command for a change in progress (for a test runner, only the tests the change touches). The builder runs foreman checks --changed --quick while it builds and the tester runs the full suite, so the long run happens once, on the finished change. A check with no quick runs as it is in both. roles.builders.<name>.fullChecks: true sets a builder back to the full run, for a project whose quick command cannot be trusted. The architect does not run the checks at all; it reads foreman checks --recorded.

In one worktree, a new run on a different tree stops an older run still going there, because the older result would be stale: --wait on the older run’s id follows the new run, and a waiter with nothing to follow (the newer one was a foreground run) is told it was superseded and starts again. Runs on the same tree never supersede each other; they share one run. Full runs across all slots are capped by checkRuns.maxParallel (default one per 8 cores, at least 1; it is checkRuns, not checks, because checks is the list of commands, and the same setting is on the settings panel under Run). A run over the cap prints “waiting for a free check slot”, starts when a seat is free, and keeps its slot’s heartbeat going meanwhile, so the watchdog sees it alive; foreman status and the dashboard show “check queued”. Quick runs and runs outside a session are never capped or superseded. Each session also gets FOREMAN_TEST_CORES (below) so several slots running tests at once do not each take every core.

Searches stay inside the project

A story only ever needs the project, so Foreman keeps sessions off the rest of your disk. The folders a session may search or read are the project (FOREMAN_PROJECT_DIR), its state folder (the slot worktrees live inside it), ~/.claude (or CLAUDE_CONFIG_DIR), the temp folder (TMPDIR and /tmp) and the plugin folder. A root that is /, your home folder or a parent of it is dropped, so a stray TMPDIR=/ cannot allow everything.

For Bash, Foreman denies a find, du, ls -R, grep -r (also egrep, fgrep, -R, --dereference-recursive and -d recurse), rg or mdfind whose start is outside those folders, / and the home folder included; mdfind without -onlyin searches the whole disk and is always denied. Each command’s own option syntax is read, so a pattern given as an option is not taken for a folder. Shell forms do not get round it: if/then, while and until loops, { }, eval, xargs, nice, env -C and a cd or pushd first are followed, and a for loop’s variable is replaced by the loop’s values when it is the only variable in the start. A start is checked as written, with no file-system call, and again once its symlinks are resolved, so a link under a private folder is never followed. A start that still holds a $ (a variable Foreman cannot expand) is skipped rather than guessed.

Foreman also denies a Read, Glob or Grep tool call outside those folders: Read’s file_path, Grep’s path (or the current folder), and Glob’s path plus the literal start of its pattern, so ../../x/** is checked too. A leading ~ is the home folder. The denial ends with the same sentence every time: search only inside the project, ~/.claude and the temp folder, and start at . or a folder in the project.

The Bash guard fails closed, like the others: a guard that cannot decide denies. The Read, Glob and Grep guard runs in the hook process itself and fails open, so a broken hook never stops every Read. The whole hook has 8 seconds to decide (FOREMAN_GUARD_TIMEOUT_MS can shorten it), inside Claude Code’s 10 second hook limit; no decision in time means deny.

Why it matters on macOS: when foreman service install (launchd) started the run, a scan of your Desktop, Documents, Photos or a network volume makes macOS ask you, on behalf of node, to allow access. Foreman never needs it: answer Don’t Allow. foreman doctor has a ‘macOS privacy prompts’ check, on macOS only, that explains those prompts when the service is installed. Your own terminal is untouched.

What a session can never do to the run

The project the run builds is the anchor, named by FOREMAN_PROJECT_DIR. Its config, settings, keys and controls are the owner’s, and a story session cannot touch them however it tries. Five guards decide every Bash call, in order; the first that denies ends the decision, and each fails closed.

  1. The production host guard keeps the tester and the reviewers off production. The hosts are ship.productionHosts when set; otherwise the hosts of ship.smoke.urls plus the Supabase project’s API and database hosts (<ref>.supabase.co, db.<ref>.supabase.co), loopback names left out. A host matches on its boundaries, a subdomain of it matches too. features.productionGuard set to false switches this guard, and only this guard, off. The builder and architect are not held by it. See Production stays yours.
  2. The project-files guard denies a command that would write one of the owner’s files: foreman.config.json, the state folder’s settings.json, the project’s envFile and foremanEnvFile (.env.local and .env.foreman by default), check-runs.jsonl in the state folder, and everything in session-config/. It reads redirect targets, tee, the destination of cp, install, ln and mv, the files of rm, unlink, truncate, sed -i and perl -i, and the scripts of sh -c and eval, with a cd moving the folder relative paths start from and symlinks resolved, so a slot’s linked .env.local counts. It also denies the Foreman commands that write those files (settings set|reset, features on|off, keys, init write, traits add, learning undo|keep, learn reopen, decide answer, run, and the owner’s controls pause, resume, stop, fixed, workers, slot, handback and next) when they run in the anchor or a worktree of it, and from any folder at all when the command changes the FOREMAN_RUN, FOREMAN_PROJECT_DIR or FOREMAN_STATE_DIR variables, because the CLI would then act on the anchor whatever folder it runs in. The same files are protected from the Write, Edit, MultiEdit and NotebookEdit tools. A read (cat, grep, source, diff) is never a write.
  3. The check guard, above.
  4. The scan guard, above.
  5. The pack’s own guard, in every stage. The Salesforce pack uses it to keep sf and sfdx off every org not in salesforce.orgs and to block deployments to production (Production stays yours). A pack guard that throws or gives no reason denies.

The Foreman CLI refuses on its own as well. The owner’s controls refuse in a story session whose FOREMAN_RUN was changed or unset while FOREMAN_STORY or FOREMAN_SLOT is still set, and in any process the run’s live supervisor started, wherever it stands and whatever its environment says, when the state folder it would act on is the run’s. foreman learning tick refuses in every story session. A guard stops agent mistakes, not a hostile process: an interpreter’s own file calls get past the blocklist, and a process that re-parented itself with every variable stripped is not caught.

What an agent does instead: a check that needs Foreman’s own commands runs in a scratch git repo under the temp folder, with the FOREMAN_* variables left as they are. Inside such a repo every Foreman command acts on that repo; from the slot it acts on the real project. A supervisor for a test starts only there, with foreman run --foreground, and is stopped with foreman stop in that repo before the agent finishes. Their prompts say all of this, and the denial message repeats it.

Environment variables in a session

The supervisor gives every story session these variables. They win over the base environment, the environment adapter’s variables for the slot and anything the pack adds, so nothing in a project can change them.

Name What it holds
FOREMAN_RUN 1. Marks a story session; every guard and refusal above keys on it. The dashboard’s artifact runs set it to an empty string on purpose, which counts as unset.
FOREMAN_PROJECT_DIR The project’s main checkout, the anchor.
FOREMAN_STATE_DIR The shared state folder in the main checkout (.foreman/ unless stateDir says otherwise). HALT, the plans, the logs, the check records and the session files live there.
FOREMAN_SLOT The slot, w1, w2 and so on.
FOREMAN_STORY The story’s id.
FOREMAN_SUPERVISOR_PID The supervisor’s process id.
FOREMAN_TEST_CORES This slot’s share of the machine’s cores for its test runs: the cores split evenly between the slots, at least 2. Foreman’s own test config reads it, and your project’s test runner can too.
FOREMAN_PLUGIN_DIR When the supervisor has it, the plugin folder the session loads with --plugin-dir, so a session runs the same copy of Foreman as the run.

The foreman command reads them by itself, so an agent never passes them. Run by hand without them, a story skill is in slot w1 of whatever checkout it is in. Foreman also sets CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS to 0 for its own use, and CLAUDE_CODE_ENTRYPOINT only for the artifacts console.

Keys: of Foreman’s own keys file (.env.foreman), only ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN reach a session, and only where the supervisor’s environment has no value for them; no other name from that file does. When the platform credits ran out and the run moved to your subscription, ANTHROPIC_API_KEY is removed from every session’s environment, so Claude Code uses the OAuth token or the login on this computer. The project’s own key file (envFile) is linked into each slot’s worktree for the project’s code to read; foreman ship loads it and the agents are told never to print it. See Settings for the switch and Files for every file named here.