repolane v0.11.0 · open source

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, and lane doctor tells you whether the guard is healthy and in force.

Start here:

What you seeGo to
lane status shows no worktree for one repo of a laneA lane start that stopped halfway
a folder under lanes/ that you deleted or moved yourselfA worktree that went missing behind git’s back
lane doctor exits non-zerolane doctor failing
a Claude session is suddenly quiet: no refusals, no [lane] line, no session-start textThe hooks fail because lane is not on PATH
a tool call came back refused and you think it should not haveA refusal you think is wrong
lane upgrade printed a pip command, or says a workspace could not be refreshedUpgrading
lane done says not finishing …lane done refusing
lane stops with run: lane init, or a lane-… command is not foundSmaller traps
you are moving from a git-clone installMoving 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, with web in its repos: list and api absent
  • no api worktree, and no api branch
  • possibly no guard wiring, because write_guard runs 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 saysWhat to run
.claude/settings.json does not run the expected lane hook commandslane doctor --fix (Repolane’s hooks rewritten, your own keys and hooks kept)
REPOLANE.md and LANE.md are version X's, not the installed onelane upgrade --refresh-only (or any lane command, which refreshes them)
not registered in planes.jsonlane doctor --fix; lane upgrade only refreshes registered workspaces
lanes/<id>: a settings.local.json is missing, or runs old hookslane resume <id>
Claude Code’s memory is not linked into .repolane/memorylane 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 removednothing: 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:

RefusedThe way through
reading or editing something outside the workspacelane ref <id> add <path> "why" — it appears at lanes/<id>/refs/<name> and becomes readable
editing a reference that was attached read-onlylane ref <id> add <path> --rw re-registers it as editable; lane ref <id> rm <name> detaches it entirely
pushing to main, master or developprefix the command with the marker the message shows you: ALLOW_DEFAULT_BRANCH_PUSH=1 git push …
reading a .env or a key filelane 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 .envleave 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 directorythe 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 pip command instead of upgrading. The package was installed with pip (into a virtual environment, say), not pipx or uv. Run the command it printed, then lane upgrade --refresh-only.
  • “PyPI cannot be reached”. --check needs 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-missing drops it). A moved workspace registers itself again the next time you run lane init or lane upgrade inside it.
  • A workspace could not be refreshed. Usually a .claude/settings.json that is not valid JSON. Mend it, then lane 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 messageWhy, and what to do
<repo> is dirty — commit it, or lane park firstuncommitted 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 firstno 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 pushedthe 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 .env that 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-sessions skips the question. delete_sessions_on_done in .repolane/config.yml sets 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.

Next steps