The team file
A team is one YAML file. This page explains its parts and how a new worktree is set up.
teams/example.yaml documents every key in comments: it is
the reference for each key, and the file to copy.
Its parts
repo,project,owner: the local checkout, its GitHub repo (owner/name), the GitHub Project the team tracks work on, and the human it works for. The prompts name them, so this is the only edit to retarget a team.repo.gateis the repo's pass/fail command: see The gate.repo.docs_repos(optional) lists up to 10 more repositories (owner/name, none equal torepo.github, none twice) that a Technical Writer seat may change;checklists them and every prompt can name them as@@DOCS_REPOS@@. fabrikito makes no checkout of them and adds no permission rules for them: see Safety. The writer clones one under its ownTMPDIR, which is outside the seat's working directory, so in a permission mode that prompts, an edit there waits for a human unless a permission rule allows it. Give the writer seat atmpdir(Temp files), so that its clones are in one known folder a rule can name.storage:volumeandroot. Every worktree is created under<volume>/<root>/. The volume must be mounted, checked from the mount table when a worktree is created; the launcher never falls back to another disk.state(optional): the team's state folder (settings, launch scripts, prompts, message log, progress notes, triage records), an absolute path or~/.... Without it,<install>/.state/<team>.up,check,say, the hooks and every seat read it from the team file, so a team run from a pinned release needs no link in each release (Running a team from a pinned release). Precedence:TEAM_STATE_DIR(throwaway teams and tests), thenstate, then the default;checkprints the folder and which of the three it is. The key moves nothing, so a team that adds it to an existing state folder moves that folder first;checkwarns when the default folder is still there and is a different one. Running seats keep the state folder they started with until they restart, and a command run with another one stops at them (sessions, below): stop the seats before the folder changes, or hand each over with the command the refusal prints.checkrefuses a relative path, the home folder or a folder holding it, a folder inside the repository, a seat's working directory, the worktree folder or a checkout of fabrikito (a folder under<install>/.state/is allowed, and only there), a volume that is not mounted, and a folder that is, holds or sits inside another team's state folder (one inother_teamsor onecheckfinds by itssettings/,launch/andprompts/). The other teamscheckfinds are the folders beside the state folder, so a state folder shared with other teams' folders in one parent treats them as other teams.sessions: tmux name prefix, an optional claudewrapper,args, and an optional defaultmodel,effortandpermission_mode. Unset means the flag is omitted and the seat inherits~/.claude/settings.json.sessions.claude.plugin(defaulttrue) loads the fabrikito plugin into every seat: see The fabrikito plugin. Two teams on one tmux server need two prefixes:upstores its state folder andrepo.githubin each session it starts, andup,down,say,flush,attach,viewandtitlesstop at a session whose stored state folder or repository is not their own, naming the value that differs and thetmux set-environmentcommand that hands the session over (a session started before these were stored has neither, and is taken for the seat until it next starts). One team file run with another state folder (TEAM_STATE_DIRonupand not ondown) is stopped the same way.permissions(deny,ask,allow), at the top level and per role: see Safety.other_teams: other teams' state folders that aren't beside this team's own, each with theobserversallowed to read its log and notes. That log does not hold the body of a message to or fromsec(#227), but it can still carry private security reports sent to the EM, so an observer treats it under rule 5 and sendssecand the EM only a pointer: see Safety.prompts.commonand a per-roleprompt(file) orprompt_text(inline). Placeholders such as@@ROSTER@@,@@GITHUB_REPO@@,@@PROJECT_URL@@and@@WORKTREE_ROOT@@are filled in per seat.prompts.project(optional): a file with your project's specifics, such as what the product is, folders that are off limits, a running instance never to touch, the gate and the merge procedure. Every seat's prompt is the common prompt, then this file, then the role's prompt and skills, so a team can keep the shipped prompts and add only this. It takes the same placeholders, may not contain a[from ...]label, and seats may not edit it.checkshows it, with the longest rendered prompt, and warns when a seat's prompt goes over 199 lines. Without it, prompts are unchanged.roles: ordered; each hastitle,workdir(projectfolder orworktree), a prompt, and optionalmodel,effort,session,kickoff,summary,skills,tmpdir(see Temp files). A role added while the team runs is started by the nextup, but the seats already running do not know it until they are restarted: see Whatupdoes, step 4.skills(per role): skills to name in that seat's prompt, each a name (plugin:namefor a plugin's skill) with an optional note on when to use it. They are listed after the role's prompt with a note that a skill may not be installed; naming one installs nothing. The example team lists commonly useful ones from the superpowers plugin and Claude Code's bundled skills.
Checked at load time
Unknown keys, missing prompt files, duplicate session names and unknown placeholders are errors at load time, before anything starts.
Text is UTF-8 whatever the locale (LC_ALL=C, cron, env -i): the team and prompt files, and the
values the launcher fills in from outside them (TEAM_SCRIPT, TEAM_LOG, TEAM_STATE_DIR,
CLAUDE_CONFIG_DIR, the home folder, the team file's own path), so non-ASCII paths and names work in
prompts, kickoffs and settings. Invalid UTF-8 in any of them is an error that names where it came from.
Temp files
roles.<id>.tmpdir sets where a seat's temp files go. The launch script exports two variables from it,
so the setting belongs to the seat and not to the tmux server, whose environment holds one value for
every team on it:
storage: { volume: /Volumes/<drive>, root: Projects/myteam-worktrees }
roles:
fs1:
tmpdir: /Volumes/<drive>/tmp/myteam/fs1
TMPDIR=<tmpdir>.mktemp "$TMPDIR/name.XXXXXX"creates its file there. Givemktempthat explicit template: a baremktempwas seen to ignoreTMPDIRon macOS.CLAUDE_CODE_TMPDIR=<tmpdir>/claude. Claude's scratchpads and task output go under it. SettingTMPDIRalone leaves them on the internal disk.
The folder must be inside storage.volume and name both the team and the role as folders. Two teams on
one host share role names, so a folder without the team can collide, and every command refuses it when
it loads the team file. check shows each role's folder and also refuses, while the volume is mounted, a
path that a symlink or another mount leads off the volume.
Loading also refuses a tmpdir that is, holds or sits inside another role's tmpdir (two roles naming
one folder included), the team's state folder, the repository or any seat's working directory, or that is
or holds the worktree folder (inside it, beside the worktrees, is fine), and as every command loads the
team file, one with such an overlap is refused by all of them after the upgrade, say and down
included: run check with the new version before a restart (#300).
up makes the folder and its claude/, owner-only, before it starts anything, for each seat it is about
to start. It refuses if the volume is not mounted, the same as for a worktree, and creates nothing: mkdir
under an unmounted /Volumes path would land on the internal disk. It also refuses a symlink at either
folder. A running seat keeps what it started with and picks up a change at its next start.
Owner-only (mode 700) guards against other OS users, and only on a volume that honours ownership. It
keeps out nobody on a volume mounted noowners (Finder's "ignore ownership on this volume"), and every
seat runs as the same user in any case. Separate folders keep two seats' temp files apart, and two
teams', by accident guard only, as with the other rules in Safety: this is not a security
boundary between seats, and any seat can read and write another seat's folder: nothing denies it, and in
a permission mode that prompts its Edit and Write tools ask first.
In a permission mode that prompts, a Write outside a seat's working directory would wait for a human, so
the generated settings give each seat an Edit allow on everything in its own tmpdir
(#252), in both spellings of the path (as configured and with links resolved). No
seat gets one on another role's or another team's folder, and a seat without tmpdir gets none. It is an
allow, the weakest list: a deny rule above it still wins. check shows it on the seat's added: line.
A role with no tmpdir behaves as before: the seat takes TMPDIR and CLAUDE_CODE_TMPDIR from whatever
started it, and check shows (inherit). There is no default folder, because the choice is explicit per role.
A team pinned to a release gets the key by moving to a release that has it: see Running a team from a pinned release.
New worktrees
A worktree up creates is a fresh checkout of repo.base: none of the main checkout's git-ignored
files are there (an .env, local config), and nothing is installed. Two optional repo keys set it up
before its seat starts, the way Claude Code's own worktrees use .worktreeinclude and an install
command (docs):
repo.copy: paths relative to the main checkout, copied into the new worktree (files or folders; meant for small files). A path that does not exist is a warning; a file the worktree already has is left as it is. Paths must stay inside the checkout. Only real files and folders are copied: a path that is or contains a symlink, or that a symlinked folder would lead out of the checkout or out of the worktree, stops the setup.repo.bootstrap: a command run with bash in the new worktree after the copy, with no terminal input and withTEAM_ROLE(the seat's role id) andTEAM_REPO(the main checkout) set, for examplenpm cioruv sync. Its output is shown, indented, as it runs. It runs without the messaging and tmux variables of a seat that runsup: every variable whose name starts withCLAUDE_CODE_MESSAGING_(todayCLAUDE_CODE_MESSAGING_SOCKETandCLAUDE_CODE_MESSAGING_TOKEN, and any that a later Claude Code adds),CLAUDECODE,TMUXandTMUX_PANE. So a seat's messaging token does not reach an install script. A role with atmpdir(Temp files) has that folder as the bootstrap'sTMPDIR, as its seat does, so an install's temp files go to the same place (#253); a role without one keeps theTMPDIRof the shell that runsup. Every other variable of the shell that runsupdoes reach it, and an install command runs third-party scripts (package install hooks, for example) with them. There is no time limit. If it fails, orupis interrupted meanwhile (Ctrl-C, TERM, HUP),upstops the bootstrap and stops before starting the seat; a worktree it has just made is removed (by that exact path), so the nextupstarts over.
When both have finished, up marks the setup done (<state>/setup/<role>, with the worktree's path).
Every later up checks that mark for a worktree it finds, and redoes the copy and the bootstrap
before the seat starts when the mark is missing: after a start that was killed or crashed midway, or
when repo.copy or repo.bootstrap was added after the worktree was made. A failure there leaves
that existing worktree as it is (it may hold work). Neither runs for a project workdir. check
shows them.
An interrupt (Ctrl-C, TERM, HUP) while git is still adding a new worktree is handled the same way, with
or without a setup: once git returns, up removes that worktree by its exact path and starts no seat
(#198).
up starts the roles in file order and stops at the first one it cannot start, this setup included:
the seats started before it keep running, and the next up starts the rest (it skips running seats).
Credentials
Do not list real credentials in repo.copy: each copy is another file on the worktree volume that a
seat works in. Use development values, or have the bootstrap fetch what it needs. The example team's
Read deny on .env files covers a copied .env like the original. That deny stops the file tools and
the file commands Claude recognises, not git show of a committed file or a recursive grep: see "What
the rules cannot do" in Safety.
Package stores
When worktrees are on another volume, tools that hard-link from a shared store (pnpm, uv) can only
link within one volume, so with the store on the internal disk and the worktrees elsewhere each
worktree gets full copies instead. Point the store at the worktree volume from the bootstrap, for
example pnpm install --store-dir /Volumes/<drive>/.pnpm-store or
UV_CACHE_DIR=/Volumes/<drive>/.uv-cache uv sync. A plain download cache (npm's) works from either
disk.