Guides
When something goes wrong
The ways Repolane actually breaks: a half-made lane, a worktree deleted behind git's back, a failing lane doctor, hooks that cannot find lane, a refusal you disagree with, an upgrade that stops, a lane done that will not finish. And what to type.
On this page
In brief. This page lists the ways Repolane breaks, what each leaves on disk and the command that repairs it. Almost nothing here is lost work: branches are ordinary git branches in
.repolane/repos/<name>. Remember two commands:lane resume <id>recreates worktrees and rewrites the guard wiring, andlane doctortells you whether the guard is healthy and in force.
Start here:
| What you see | Go to |
|---|---|
lane status shows no worktree for one repo of a lane | A lane start that stopped halfway |
a folder under lanes/ that you deleted or moved yourself | A worktree that went missing behind git’s back |
lane doctor exits non-zero | lane doctor failing |
a Claude session is suddenly quiet: no refusals, no [lane] line, no session-start text | The hooks fail because lane is not on PATH |
| a tool call came back refused and you think it should not have | A refusal you think is wrong |
lane upgrade printed a pip command, or says a workspace could not be refreshed | Upgrading |
lane done says not finishing … | lane done refusing |
lane stops with run: lane init, or a lane-… command is not found | Smaller traps |
| you are moving from a git-clone install | Moving from a git-clone install |
A lane start that stopped halfway
lane start ABC-123 web api creates one worktree per repo, in order, in a single loop. Before the
loop it checks that the id is a usable branch name and that every repo named exists in
.repolane/registry/repos.yaml. It cannot check the base in advance: for each repo in turn, lane-start
fetches and looks for origin/<base>, then <base>, then a tag of that name, and stops the whole
command if none resolve. So the usual way to get half a lane is a repo whose default branch differs
from the others, or a @<base> spelled from memory.
The cleanup trap is deliberately conservative: it removes the lane folder and the lane file only
if nothing was created (an empty lanes/<id>/). If the first repo’s worktree exists, everything
stays. After a failure at the second repo you have:
lanes/<id>/web— a real worktree on a real new branch.repolane/registry/lanes/<id>.md— the lane file, withwebin itsrepos:list andapiabsent- no
apiworktree, and noapibranch - possibly no guard wiring, because
write_guardruns after the loop, not inside it
lane status reads the frontmatter, so it shows the repos that were registered and nothing about the
one that failed. If the guard wiring never ran, lane doctor reports it (see below).
To finish it, fix the base and add the missing repo to the lane that exists:
lane with <id> api # or: lane with <id> api@develop
lane with is lane-start --add: it appends to the same lane file and re-runs the guard wiring for
the whole lane. The failure message prints the closest matching branch names in that repo.
To unwind it instead, remove what was made. The first two commands are what lane done --delete-branches runs for each repo; type them by hand because lane done refuses a branch that
was never pushed:
git -C .repolane/repos/web worktree remove lanes/<id>/web
git -C .repolane/repos/web branch -D <id>
rm -rf lanes/<id> .repolane/registry/lanes/<id>.md
Do that only when the branch holds nothing you want; -D deletes it merged or not.
A worktree that went missing behind git’s back
If you rm -rf or move a folder under lanes/, git does not find out. The registration stays in
.repolane/repos/<name>/.git/worktrees/<id>/, and creating the worktree again fails because the branch is
“already checked out”. lane status shows the repo’s line as no worktree (lane-resume recreates).
The command is lane resume <id>. It first runs git worktree prune in every base clone, which
clears the stale registrations, then checks the branch out again at lanes/<id>/<repo>. It also
relinks the repo’s env files into the new worktree, re-anchors the lane folder, and rewrites the
guard. You rarely need to run git worktree prune yourself.
lane nested is a different tool for a different fault: a worktree inside another worktree, which
is broken because the outer repo sees the inner one’s files as its own. lane nested lists them;
lane nested --fix removes them, skipping any with uncommitted work, and never touches the branch.
lane status surfaces the same check. So: missing worktree: lane resume; worktree inside another:
lane nested --fix.
lane doctor failing
lane doctor makes four separate claims; a non-zero exit means one failed, and the output says which.
The stale-lane list and the other advisories at the bottom never fail the command.
0. The workspace is not wired
The first block, Workspace <folder>:, checks what the hooks depend on. Each !! line carries its fix, and
lane doctor --fix applies the safe ones (it asks before editing CLAUDE.md or anything under ~/.claude;
--yes answers yes).
The hooks fail because lane is not on PATH
!! `lane` is not on PATH: every hook in this workspace runs `lane hook …`, so every hook fails and Claude Code runs tools UNGUARDED
fix: put the folder lane is installed in on PATH (in your shell profile), then open a new terminal: export PATH="…:$PATH"
This is the most serious line the doctor can print. Every hook in .claude/settings.json and in every
lane is a lane hook … command, run by Claude Code through your shell’s PATH. When lane cannot be
found, the hook cannot start, and Claude Code treats a hook that fails to start as no objection: the
guard fails open, every tool call goes through, and nothing on screen says so. No refusals, no [lane]
line on prompts, no session-start text: if a session is suddenly that quiet, this is why.
It happens after a pipx install whose bin folder (usually ~/.local/bin) is not on PATH
(pipx ensurepath adds it), or when Claude Code is started from an app or launcher that does not read
your shell profile. Add the export line the doctor prints to your shell profile, open a new terminal,
start Claude Code from it, and run lane doctor again. Running the doctor itself by full path
(~/.local/bin/lane doctor) works when lane is not on PATH.
Repolane’s rules do not load: the import line is missing
!! CLAUDE.md does not import .repolane/REPOLANE.md: Repolane's rules do not load here
fix: add the line @.repolane/REPOLANE.md to CLAUDE.md, or run: lane doctor --fix (it asks first)
The hooks still guard every tool call, but the session never reads the rules: it does not know about
lanes, the lane-* commands or how to save memory, and works against the refusals instead of with
them. lane init adds the line to an existing CLAUDE.md only after you say yes, and with no terminal
and no --yes it leaves the file alone, so this is what you see after lane init ran in a script, or
after you answered no. Add the line anywhere in CLAUDE.md yourself, or run lane doctor --fix.
The other workspace lines
| What it says | What to run |
|---|---|
.claude/settings.json does not run the expected lane hook commands | lane doctor --fix (Repolane’s hooks rewritten, your own keys and hooks kept) |
REPOLANE.md and LANE.md are version X's, not the installed one | lane upgrade --refresh-only (or any lane command, which refreshes them) |
not registered in planes.json | lane doctor --fix; lane upgrade only refreshes registered workspaces |
lanes/<id>: a settings.local.json is missing, or runs old hooks | lane resume <id> |
Claude Code’s memory is not linked into .repolane/memory | lane doctor --fix (asks first). One you declined is a note, not a failure |
a note about a slash command or agent in .claude/: your own file, one you edited, or one you removed | nothing: Repolane leaves those as they are and only replaces files still exactly as it wrote them |
1. The guard does not compile
lane doctor runs python3 -m py_compile on the installed package’s scripts/hooks/guard.py, and
prints the syntax error. This matters most: a hook that cannot run refuses nothing, so every rule is off
at once and sessions look normal (see how the guard fails). The guard
belongs to the package, so the fix is to put the package back: pipx reinstall repolane (or
uv tool install --reinstall repolane). If you are working on the guard itself, in a checkout of the
Repolane repo, bash scripts/hooks/test-guard.sh there runs the full suite against a throwaway setup of
its own and does not touch your lanes.
2. A probe answers wrongly
The guard compiles but a probe answers wrongly. lane doctor feeds the guard eleven named
probes and checks each answer. Ten must come back deny: reading a .env, a shell that prints
one, a Grep over a .env, a push to a default branch, a push to main written as git push origin 'main';,
rewriting a remote, deleting a GitHub repository, writing a memory file directly, and two ways of
entering a worktree. The eleventh must come back pass: plain git status has to be left alone,
because a guard that refuses everything would pass the other ten and be useless. A failing probe is
named with what was wanted. The cause is a rule that stopped refusing: a changed guard file in the
installed package (reinstall it), or, when the probe that fails is one of yours, a rule in
.repolane/registry/rules.yaml that no longer matches its example.
3. A lane or worktree is not pointing at the guard
The first checks cannot see this. lane doctor walks every lane folder, every worktree inside it and
the workspace root, and checks that the .claude/settings.local.json there (at the root,
.claude/settings.json) has the lane hook … entries that reach the guard. It also fails a place whose PreToolUse matcher lacks
Glob|Grep (“so searches skip the guard”); lane resume <id> rewrites it. It prints a count
(7/7 places point at guard.py: …) and names any that do not. Those files are per-lane machine
state, kept out of git, so another tool can overwrite them, you can edit or lose them, or they may
never be written for a worktree that appeared by another route. Such a lane is unguarded while
guard.py is healthy, which is why the check exists.
lane resume <id> # rewrites settings.local.json for that lane and every worktree in it
lane doctor -v lists every location it checked, wired or not, then runs the full guard suite.
lane status runs the quiet form at the top of its output. “Doctor is green” means the rules are in
force, not that nothing can get past them; the security model says where they stop.
A refusal you think is wrong
First, work out what refused you. A Claude Code permission prompt asks and waits. A guard refusal
does not ask: the call comes back already denied, with a paragraph written by Repolane that names the
lane you are in, names a lane-… command to type, and often names you (the guard substitutes the
owner from .repolane/config.yml). The other refusal that does not ask is Claude Code’s own
permissions.deny in the lane’s settings: one line (“File is in a directory that is denied by your
permission settings”), covering Read and Edit of .repolane/repos/**, other lanes and the secret names.
No prompt and a paragraph means the guard.
Every refusal has a documented way through, and the message names the one that applies:
| Refused | The way through |
|---|---|
| reading or editing something outside the workspace | lane ref <id> add <path> "why" — it appears at lanes/<id>/refs/<name> and becomes readable |
| editing a reference that was attached read-only | lane ref <id> add <path> --rw re-registers it as editable; lane ref <id> rm <name> detaches it entirely |
pushing to main, master or develop | prefix the command with the marker the message shows you: ALLOW_DEFAULT_BRANCH_PUSH=1 git push … |
reading a .env or a key file | lane keys <path> prints the variable names and never the values, from your own terminal (the guard refuses it inside a session because the command line names a secret file). Inside a session only the bare lane-env-keys <path> passes the guard. Point the session at .env.example and the other .example/.sample/.template/.dist files, which are ordinary readable files; lane env <id> checks each worktree of a lane has the env files its base clone does, by name only |
a recursive search (grep -r, rg, tar, cp -r, the Grep tool) over a repo that has a .env | leave the secret out: grep -rn TODO api --exclude='.env*', rg -g '!.env*', an --include='*.py', a subfolder, or for the Grep tool a glob of "!.env*". The denial names the option for that tool |
cd <repo>, pushd, a cd to a computed directory | the session stays at the lane root: git -C <repo> …, lane-run <repo> <cmd…>, <repo>/path |
These are escape hatches by design. lane ref records what it attached in the lane file; the push
marker shows in the transcript and nowhere else. A session can type either, which is why the marker is
advisory (see the security model).
Rules lists every refusal with the failure behind it, and the
FAQ covers changing the rule itself.
If something got through that you expected to be refused, that is usually the shape of the mechanism, not a bug; see what the guard does not check.
Upgrading
lane upgrade upgrades the package and then refreshes every registered workspace;
lane upgrade --check shows what it would do and changes nothing. What can go wrong:
- It printed a
pipcommand instead of upgrading. The package was installed with pip (into a virtual environment, say), not pipx or uv. Run the command it printed, thenlane upgrade --refresh-only. - “PyPI cannot be reached”.
--checkneeds the network; nothing was changed. - A workspace “is gone”. A registered folder was moved or deleted. It is skipped, and you are asked
whether to drop it from
~/.local/share/lane/planes.json(--drop-missingdrops it). A moved workspace registers itself again the next time you runlane initorlane upgradeinside it. - A workspace could not be refreshed. Usually a
.claude/settings.jsonthat is not valid JSON. Mend it, thenlane upgrade --refresh-only. The exit status stays non-zero until every workspace is refreshed.
A workspace that was not refreshed by lane upgrade still catches up on its own: the first lane
command or hook after the installed version changes rewrites .repolane/REPOLANE.md and LANE.md.
Run lane doctor after an upgrade.
Moving from an older git-clone install has its own page:
Moving from a git-clone install, including what to do when the clone’s own
lane upgrade refuses.
lane done refusing
lane done <id> checks every repo in the lane before it removes any of them, and prints
not finishing <id> — nothing was removed: with a line per problem.
It refuses on the things below. “Unpushed” means commits on HEAD that no origin/* branch has,
so a plain git push origin <id> (no -u) is enough.
| The message | Why, and what to do |
|---|---|
<repo> is dirty — commit it, or lane park first | uncommitted changes in that worktree. Commit them, or lane park <id> "<note>", which wip-commits every dirty worktree on its own branch and records a resume note. Untracked secret-named files are not counted as dirty (park leaves them out on purpose); they are reported separately, below |
<repo>/<branch> was never pushed (N unpushed commit(s)) — git push origin <branch>, or delete the branch first | no origin branch has the work. The message names the push. If you pushed to a fork instead, rerun with --pushed-to <remote> |
<repo>/<branch> has N unpushed commit(s) — git push origin <branch> | the branch is ahead of what any origin branch has. Push it |
<repo>: HEAD is detached with N commit(s) not on origin — … | git switch -c <name> and push it, or git push origin HEAD:refs/heads/<name> |
<repo> has no remote called '<name>' (--pushed-to) | you passed --pushed-to a remote the repo does not have |
<repo> has untracked secret file(s) still on disk … that deleting the worktree would destroy: … | typically what lane park left out. Move them into .repolane/repos/<repo>/ or somewhere safe, or rerun with --discard-ignored to delete them with the worktree |
<repo> has ignored secret file(s) that deleting the worktree would destroy: … | a real (non-symlink) ignored .env or key in the worktree. Move it into .repolane/repos/<repo>/ (lane start links env files from there) or pass --discard-ignored. Symlinks made by lane start, and anything inside node_modules, .venv, vendor, dist, build, .git and similar dependency or build folders, are skipped |
the worktree is missing and the branch is not fully pushed | the folder is gone, so lane done cannot inspect it, and the branch still has commits origin does not have. lane resume <id> recreates the worktree first; then finish normally |
A repo with no origin (added from a local-only checkout) does not block: lane done notes
nothing to push, the branch <branch> stays in .repolane/repos/<repo> and finishes. A repo with no commits yet is noted too.
When the checks pass, lane done removes each worktree, prints any ## Findings from the lane file
so you can move what is still true into repo memory, and moves the lane file to
.repolane/registry/lanes/done/. Branches are kept unless you pass --delete-branches. Know what removing a
worktree takes with it:
- Git-ignored files inside the worktree go too (build output, anything else ignored); the “clean”
check does not count them. A real ignored or untracked secret-named file (a local
.envthat is not the symlink) blocks the finish until you move it or pass--discard-ignored. - Claude’s saved sessions for each worktree (
~/.claude/projects/<slug>/) are deleted only if you answer yes at the prompt, or pass--delete-sessions. With no terminal it leaves them and says so.--keep-sessionsskips the question.delete_sessions_on_donein.repolane/config.ymlsets which way the prompt defaults.
lane park is the other ending. It is for work you are stopping rather than finishing: it
commits the loose changes, writes the resume note (at least 10 characters), and sets the lane
parked. Nothing is removed, and lane resume <id> picks it back up. It commits every change that is not
git-ignored — modified and deleted tracked files, and untracked files — except files whose name says secret
(.env*, id_rsa, *.pem, secrets.yml and the rest of the guard’s list),
which it leaves on disk and names. It does not look at content, so a credential under an ordinary name would
still go into the commit: look at git status -s in each worktree before you park.
Smaller traps
Nothing works outside a workspace. Any command other than init and help, run in a folder that
is not inside a workspace, stops and tells you to run lane init. lane looks for .repolane/ in the
current folder and every folder above it. cd into your workspace (or set LANE_WORKSPACE), or run
lane init to make this folder one. lane init is safe to re-run at any time.
Commands the docs mention are “command not found”. The guard’s messages and the rules use the long
names: lane-start, lane-run, lane-memory. The package installs them next to lane, so if lane
works and they do not, an old install is in the way: pipx reinstall repolane. If lane is not found
either, see the hooks fail because lane is not on PATH.
A lane session cannot save its Goal or a memory. Inside a lane, Claude Code refuses Edit and Write
on any path outside the lane folder (“This session is isolated in the worktree”), and the lane file and
the memory folders are outside it. That refusal is Claude Code’s own and no setting turns it off. The
session should run lane-memory goal "<text>" and lane-memory draft <slug> "<text>" instead;
.repolane/LANE.md tells it so. If it keeps trying Edit, check that the import line is in CLAUDE.md
(above).
lane env says an env file is missing. Env files are gitignored, so a fresh worktree has none;
lane start and lane resume symlink them in from the base clone. A worktree created before you
added a .env to .repolane/repos/<repo> does not have the link; lane resume <id> adds it.
A base clone is dirty or off its default branch. lane status flags both. Every worktree shares
the base clone’s object store, so it is not a place to work. Move the changes into a lane, then put it
back with git -C .repolane/repos/<repo> stash and git -C .repolane/repos/<repo> checkout main.
Reporting something
If none of this covers it, open an issue at
github.com/aj-oss-tools/repolane/issues with the
output of lane --version, lane doctor -v and lane status. None of these three prints the contents
of a secret file; check the paths before you post, since they include your own directory names.