fabrikito.
REFERENCE · main at 66c43e7 Edit on GitHub ↗

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.gate is the repo's pass/fail command: see The gate. repo.docs_repos (optional) lists up to 10 more repositories (owner/name, none equal to repo.github, none twice) that a Technical Writer seat may change; check lists 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 own TMPDIR, 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 a tmpdir (Temp files), so that its clones are in one known folder a rule can name.
  • storage: volume and root. 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), then state, then the default; check prints 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; check warns 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. check refuses 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 in other_teams or one check finds by its settings/, launch/ and prompts/). The other teams check finds 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 claude wrapper, args, and an optional default model, effort and permission_mode. Unset means the flag is omitted and the seat inherits ~/.claude/settings.json. sessions.claude.plugin (default true) loads the fabrikito plugin into every seat: see The fabrikito plugin. Two teams on one tmux server need two prefixes: up stores its state folder and repo.github in each session it starts, and up, down, say, flush, attach, view and titles stop at a session whose stored state folder or repository is not their own, naming the value that differs and the tmux set-environment command 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_DIR on up and not on down) 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 the observers allowed to read its log and notes. That log does not hold the body of a message to or from sec (#227), but it can still carry private security reports sent to the EM, so an observer treats it under rule 5 and sends sec and the EM only a pointer: see Safety.
  • prompts.common and a per-role prompt (file) or prompt_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. check shows 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 has title, workdir (project folder or worktree), a prompt, and optional model, effort, session, kickoff, summary, skills, tmpdir (see Temp files). A role added while the team runs is started by the next up, but the seats already running do not know it until they are restarted: see What up does, step 4.
  • skills (per role): skills to name in that seat's prompt, each a name (plugin:name for 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. Give mktemp that explicit template: a bare mktemp was seen to ignore TMPDIR on macOS.
  • CLAUDE_CODE_TMPDIR=<tmpdir>/claude. Claude's scratchpads and task output go under it. Setting TMPDIR alone 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 with TEAM_ROLE (the seat's role id) and TEAM_REPO (the main checkout) set, for example npm ci or uv sync. Its output is shown, indented, as it runs. It runs without the messaging and tmux variables of a seat that runs up: every variable whose name starts with CLAUDE_CODE_MESSAGING_ (today CLAUDE_CODE_MESSAGING_SOCKET and CLAUDE_CODE_MESSAGING_TOKEN, and any that a later Claude Code adds), CLAUDECODE, TMUX and TMUX_PANE. So a seat's messaging token does not reach an install script. A role with a tmpdir (Temp files) has that folder as the bootstrap's TMPDIR, as its seat does, so an install's temp files go to the same place (#253); a role without one keeps the TMPDIR of the shell that runs up. Every other variable of the shell that runs up does 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, or up is interrupted meanwhile (Ctrl-C, TERM, HUP), up stops the bootstrap and stops before starting the seat; a worktree it has just made is removed (by that exact path), so the next up starts 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.