Troubleshooting
Each entry gives what you see, why it happens in a line, and what to do. The page it links to explains the mechanism and has the exact rules.
Starting a team
up stops: "storage volume ... is not mounted"
The launcher never puts a worktree on another disk. Mount the volume named in storage.volume, then
run up again. See The team file.
up stops: "refusing to start seats for ... on the default tmux server"
The team file is outside teams/, so the launcher takes it for a throwaway copy, and a copy would
reach the real team's seats. down, say, flush, attach, view and titles refuse it the same way. Run
a test team on a private server with test/live-env.sh, as the message shows. If the team is meant to
run on the default server, pass --default-socket. See Throwaway teams.
up stops: "'' already has a worktree elsewhere"
The repository has a worktree with this seat's folder name at another path, from an older layout for
example. Stop the seat, then run ./fabrikito migrate <role>, which removes that worktree so that
up makes a new one in the right place. migrate refuses, and says why, a worktree with uncommitted
or untracked files, a branch checked out, or commits that are not in the base, and any worktree a
running process works in: move that work by hand first. See The commands.
up stops: "cannot list the worktrees ... (needs git 2.36 or later)"
The launcher reads worktree paths with git worktree list -z, which git 2.36 added. Update git. See
Requirements.
up stops: "claude not found on PATH" or "could not read the version"
Claude Code is not installed, is not on the PATH of the shell you run fabrikito from, or up
caught it in the middle of a reinstall. Install Claude Code or add it to that PATH; after an update,
wait for it to finish. Then run up again.
check or up says the team file "is not valid YAML: ... found unexpected ':'"
The system ruby (2.6) does not accept an unquoted URL inside a YAML flow mapping, such as
project: { name: Board, url: https://... }. Quote the URL, or write the mapping in block style, as
teams/example.yaml does.
up prints "ATTENTION is not at its input box (a startup dialog?)"
A seat started in a folder Claude Code has not seen stops at a trust dialog until a human answers it
once (and the first run in bypass mode shows a warning). Run ./fabrikito attach <role>, answer the
dialog, and detach with ctrl-b d. See Safety.
The iTerm view does not open: "osascript timed out after 30s"
The first run asks macOS for permission to control iTerm, and a permission prompt that nobody has
answered blocks AppleScript. Answer the prompt (or allow your terminal to control iTerm under
System Settings, Privacy & Security, Automation), then run ./fabrikito view. The seats are running
either way. See The iTerm view.
A seat gets "Operation not permitted" on ~/Downloads or a removable volume
A seat started from tmux has no responsible app, so macOS does not give it the privacy grants your
terminal has. Set sessions.claude.wrapper to a program that holds the grants and runs claude. A
shell function in ~/.zshrc does not help: the seat's launch script runs under bash. See
The team file.
Every worktree gets a full copy of its packages
Tools that hard-link from a shared store (pnpm, uv) can link only within one volume. With the store on
the internal disk and the worktrees on another volume, each worktree gets full copies. Point the store
at the worktree volume from repo.bootstrap. See Package stores.
Running seats
A seat is silent and does not answer
It may be waiting at a permission prompt: an ask rule still prompts in every mode, bypass
included, and a seat at a prompt waits for a human. say will not deliver to it there. Attach to the
seat and answer the prompt. See Safety.
A prompt names a rule, but its reason is about parsing
A prompt such as Permission rule ... requires confirmation, with a reason like "expansion
obfuscation" or "shell operators that require approval", means the command's text matched an ask
rule. Claude Code could not parse the command, so it matched the rule against the whole text. Run
git push origin <branch>:<branch> as a command of its own, and avoid a subshell ( ... ). See
Safety.
status shows a worktree as "(missing)"
The seat's worktree folder is not there, for example because the storage volume is not mounted. Mount the volume. A worktree seat should stop and tell the EM rather than work elsewhere. See The commands.
A seat starts without its progress note
With disableAllHooks: true in ~/.claude/settings.json, seats without a gate get no session-start
hook, so no note and no message logging. check prints a hooks line naming them. See
Progress notes.
A seat keeps being sent back by the gate
The gate fails at each stop while the seat has uncommitted changes. Read the full log,
.state/<team>/gate/<role>.log, and fix the cause; "gate timed out" means the gate ran past
repo.gate_timeout. If the seat is stopping on purpose (waiting for an answer, or with a test red
before the fix), it should say so in its report. See The gate for the limit
on send-backs.
Messages
check warns: "seat-to-seat messages will be held"
Some seats run in bypassPermissions and others in a prompting mode. Messages between the two
classes wait at an approval dialog in the receiver's pane and are dropped when it expires, so in an
unattended team they are lost. Give every seat a permission_mode of the same
class, or set crossSessionInbound. See Which messages are held.
say refuses to send
say types into a seat only when the seat's screen is Claude Code's input box, ready for input, and it
gives the reason when it refuses:
- "screen not recognised as ready": a permission prompt, a menu or a dialog, the held-message dialog included. Attach, deal with it, and send again.
- "the input box ... is not empty": someone is typing, or an earlier message was not sent. Wait and
try once more later; do not loop (a repeat of the same text within 300 seconds is refused as a repeat,
and
statusshows a notice for the seat after three refusals). If the text is a fragmentsayitself left,./fabrikito flush <role>submits it, deleting nothing; text someone typed is theirs, and the owner can attach and submit or clear it. Seesay. - "in copy mode": someone is scrolling. Leave copy mode with q.
- "shows Claude Code's diff panel notice": the pane is too short, even after
saygrew it for the check. Make it 110 columns wide or more, or 16 rows or more, or close the diff panel in the seat. - "has no claude process running (its pane is at a shell)": the text would run as a shell command.
Start the seat again (
down --yes <role>, thenup <role>). - "is down": the seat is not running. Start it with
up <role>. - "the message is empty or only whitespace": the text given to
saywas empty, often an unset variable in a script. Nothing was typed. Check what the caller passes. - "another say is typing into
and did not finish within 8 seconds": try again. "is not a regular file: remove it": something other than a plain file sits at the seat's lock path, .state/<team>/say/<role>.lock; remove it. - "does not show the message after the paste ... text left in input box": no Enter was sent, and
the text goes out with the next Enter in that seat. If it is
say's own text,./fabrikito flush <role>submits it (one Enter, nothing deleted); otherwise attach and send or clear it. - "does not show just the message's label after its paste ... so the message was not pasted": after
pasting
[from <role>],sayfound something else in the box (the label cleared or swallowed, other text, a dialog), so it did not paste the message. Attach, clear the box, and send again. - "the message's label was pasted into
but the message was not": tmux failed between the label's paste and the message's (the seat's session ended, for example). Only [from <role>]is in the box, and no Enter was sent. Attach and clear it, then send again. "could not paste into the input box": tmux failed before anything reached the box. Send again.
A refused message is logged as NOT-DELIVERED. If say refuses every seat after a Claude Code update,
the update has changed how the input box is drawn, and fabrikito needs updating. See
say.
After a Claude Code update
status shows REPLACED, or up prints "ATTENTION ... has been replaced"
The executable that seat runs was swapped under it, and macOS may stop its privacy grants (the
worktree volume, for example). Restart the seat: ./fabrikito down --yes <role>, then
./fabrikito up <role>. To update every seat, run claude update in your own shell, then
down --yes and up. See Updating Claude Code.
up stops: "... is not a copy of ..."
The file at the team's copy of claude, .state/<team>/bin/claude-<version>/claude, is runnable but
differs from the shared executable of that version, or is a hard link (a file with a second name), so
up would have every seat run something that may not be that claude. Either it was put there (by a
seat or a person: find out which before going on), or the shared claude changed without a new version,
as a reinstall under way can: then retry up once it is done. up does not replace the file, because
a running seat may be using it. Once status shows no seat running it, remove that exact file and run
up again, which makes a fresh copy (#208). A symlink there is refused the same
way (#194).
.state/<team>/bin/ takes a lot of disk space
Each old copy of claude holds about 310 MB once the shared file has moved on, and nothing removes
them. When check lists a copy with no running seat, remove that folder by its exact path. See
Updating Claude Code.
Tests and live tests
test/e2e.sh fails one assertion with "echo: write error: Interrupted system call"
Under heavy load, macOS's bash 3.2 does not restart a pipe write that a child's exit interrupts (#101). Rerun the test. This is the only failure where a green rerun is the answer: for any other failure, a green rerun is not evidence. See The one failure to rerun.
A test command reached the real team's tmux server
tmux follows $TMUX when it is set, and falls back to the default server, without a word, when
TMUX_TMPDIR is empty or names a folder that does not exist. Run live tests through
test/live-env.sh, which unsets TMUX, points TMUX_TMPDIR at a folder of its own and checks that
folder on every call. It also puts a guard first on PATH, which fails any tmux call that would use a
socket outside that folder. See Live tests.
For contributors
These cost time when changing fabrikito or its tests.
- A tmux target written
=namematches the session name exactly, but pane and option commands need a trailing colon (=name:). In zsh, quote it: zsh expands an unquoted=nameto a command path and fails with "name not found". pane_current_commandsaysbashwhile claude runs under a bash launcher. Tell whether claude runs from the processes on the pane's tty instead.saymust refuse a pane at a shell (the text would run) and one at a startup dialog (the Enter would pick "No, exit").- System ruby is 2.6: no endless method definitions.
- A view window stretched across every display comes from sizing it with Finder's desktop
bounds, which span all displays. Window sizing uses NSScreen (
lib/screen_bounds.js) instead. - Scripting iTerm needs a macOS Automation grant, and a pending prompt blocks
osascriptforever, so run it with a time limit.