repolane v0.11.0 · open source

Reference

Commands

Every Repolane command, what it does, and when you reach for it.

On this page

In brief. Every command is one word after lane; bare lane is lane status. This page lists each command by what you use it for, then exit codes, lane doctor, environment variables and the long-form names. Remember that the guard itself is a guardrail with stated limits (Security model), and that a command which behaves differently from this page is a bug in the page or the tool.

Every command is one word after lane. Run lane on its own for the status of everything going on right now (the same as lane status), and lane help for this list in your terminal. Every command works on the workspace that contains your current folder (or the one LANE_WORKSPACE names). Outside a workspace, every command except init, migrate, help and --version stops with “this folder is not a workspace yet / run: lane init” and exits 2.

     web/SHOP-412: 0 dirty, 1 unpushed
     api/SHOP-412: 0 dirty, 1 unpushed
  SHOP-418 [active] updated 2026-10-22 ticket=
     mobile/SHOP-418: 1 dirty, 0 unpushed
     api/SHOP-418: 2 dirty, 0 unpushed
== drift: checkouts outside the workspace (should be none; work happens here)
~/work $ lane brief SHOP-412
━━ SHOP-412  [active]  ticket: SHOP-412  updated: 2026-10-22
GOAL
  Promo codes are validated on the client only, so a crafted request can apply any discount.
  Move the calculation server-side and have the client display what the api returns.
RESUME NOTE
  _2026-10-22_: promo math moved to api; web still calculates locally
REPO web @ SHOP-412
  worktree: /Users/john.mercer/work/lanes/SHOP-412/web
  dirty: 0   unpushed: 1
  f54c566 wip: park SHOP-412 2026-10-22
  c297c4a Initial commit
  memory index (/Users/john.mercer/work/.repolane/memory/web/MEMORY.md):
REPO api @ SHOP-412
  worktree: /Users/john.mercer/work/lanes/SHOP-412/api
  dirty: 0   unpushed: 1
  b915b32 wip: park SHOP-412 2026-10-22
  edac2f0 Initial commit
  memory index (/Users/john.mercer/work/.repolane/memory/api/MEMORY.md):
LOG (last 5)
  - 2026-10-22 started
  - 2026-10-22 parked: promo math moved to api; web still calculates locally
~/work $ exit

The last screen of a real asciinema recording, as text. With JavaScript the player replays the whole session from the start.

lane status across three repos and two lanes, then lane brief on one of them — the goal, the note left behind, and where each repo stands. Recorded 2026-10-22. Output can differ from what you see today; run it to see the current text.

Output from real runs of the commands in a demo workspace, for the five you reach for most on day two and after. The ticket ids, repo names and owner are made up and home-directory paths are shortened to ~; nothing else is edited. The captures are recordings: probe counts and message wording in them can differ from what you see today. The lane doctor section below shows current output.

Every base clone, every lane, what is dirty, what is unpushed — the whole picture in one screen, with no model call and no waiting.

lane status
$ lane status
guard: 11/11 probes answered as they should, 14/14 places point at guard.py: 4 lane(s), 9 worktree(s), the root  —  lane doctor for the evidence
== workspace (its own git repo, if you made one: docs/install.md)
  not a git repo — memory/, knowledge/, CLAUDE.md are not tracked anywhere
== repos (primary mirrors)
  api: main, behind origin by 0, worktrees=3
  billing-core: main, behind origin by 0, worktrees=1
  mobile: main, behind origin by 0, worktrees=1
  web: main, behind origin by 0, worktrees=2
  worker: main, behind origin by 0, worktrees=2
== lanes
  INFRA-661 [parked] updated 2026-10-22 ticket=
     worker/INFRA-661: 0 dirty, 2 unpushed
     api/INFRA-661: 0 dirty, 0 unpushed
  PAY-4155 [active] updated 2026-10-22 ticket=
     web/PAY-4155: 2 dirty, 1 unpushed
  PAY-4182 [active] updated 2026-10-22 ticket=PAY-4182
     worker/PAY-4182: 1 dirty, 1 unpushed
     billing-core/PAY-4182: 0 dirty, 1 unpushed
     api/PAY-4182: 2 dirty, 1 unpushed
  PAY-4207 [active] updated 2026-10-22 ticket=
     api/PAY-4207: 0 dirty, 0 unpushed
     mobile/PAY-4207: 1 dirty, 2 unpushed
     web/PAY-4207: 2 dirty, 2 unpushed
== drift: checkouts outside the workspace (should be none; work happens here)

Setting up

pipx install repolane (or uv tool install repolane)install the package, once per machine. It puts lane and the long-form names on your PATH. See Install
lane init [<folder>] [--owner <name>] [--yes] [--git] [--claude-home <dir>]make the folder (default: the current one) a workspace, or check that it already is one; safe to re-run. Asks for your name, whether to add the import line to an existing CLAUDE.md, and whether to link Claude Code’s memory under ~/.claude into .repolane/memory. --yes takes the defaults; --git makes the workspace a git repo with a .gitignore for the machine-local parts. See Install
lane add <git-url|path> [name]bring a repo under management. A path adopts an existing checkout (see Install); one with no origin is registered local-only. --local <path> tells it a checkout of this repo already exists on the machine, so its past conversations are recognised. --from <path> copies the git database from that checkout instead of cloning again, even when you give a URL first. It asks before linking the new base clone’s Claude Code memory (--yes accepts; with no terminal it changes nothing under ~/.claude)
lane importbring past Claude conversations in. Entirely optional; copies transcripts into a lane’s folder and never touches the originals. lane import --list only looks (and exits 1 when there is nothing to show); lane import --into <id> <n>… is the non-interactive form; lane import --claude-home <dir> reads another Claude data folder
lane migrate [<folder>] [--dry-run] [--yes] [--git] [--verbose], lane migrate --undo <backup>turn a git-clone install into a workspace, in place, with a preview, a backup and an undo. See Moving from a git-clone install
lane help, lane <command> --help, lane --versionthe full list; the usage comment directly under that script’s shebang; the version (for example lane 0.10.0)

Doing the work

lane start <id> [--base <ref>] <repo>[:<branch>][@<base>]…start a piece of work: a branch and a worktree in each repo. An existing branch of that name is checked out as it is and the base is ignored
lane with <id> <repo>[:<branch>]…add repos to work already started
lane resume <id>pick work back up: prunes stale worktree records, recreates missing worktrees, rewrites the guard wiring and marks the lane active (rewriting the wiring is how a lane created earlier picks up the current tool matcher and secret-name deny list)
lane park <id> "<note>"leave work for later: wip-commit every dirty worktree and record the note (at least 10 characters). It stages by explicit literal path and never commits a file whose name says secret (the full list is under Rules): those stay on disk, untracked, and it prints left out of the commit (secret file names, still on disk): …. A secret somebody already staged stops the park with nothing committed (git reset -- <file>, then park again). Everything else git does not ignore is committed, so look at git status first
lane done <id> [--delete-branches] [--delete-sessions|--keep-sessions] [--discard-ignored] [--pushed-to <remote>]finish and clean up. Refuses while anything is dirty or unpushed, where “unpushed” means commits on HEAD that no origin/* branch has. --pushed-to <remote> checks that remote instead of origin (a fork). A repo with no origin is local-only and does not block: it is noted and the lane finishes. It also refuses while a worktree holds an ignored or untracked secret-named file that deleting the worktree would destroy, naming it, unless you pass --discard-ignored. See what it removes and lane done refusing

A repo spec can carry a branch and a base: lane start ABC-123 web:feature/login api@release-2 puts web on branch feature/login (checked out as it is if it already exists, otherwise created from the default branch) and branches api from release-2 under the name ABC-123.

Seeing where things stand

lane or lane statusevery repo, every lane, any drift, and the workspace’s own git state. Runs the quiet form of lane doctor first, and runs git fetch -q origin in every base clone under .repolane/repos/ (a failed fetch is ignored), which is the only network access it makes.
lane brief [<id>]catch up on one piece of work. For a finished lane it prints an “archived” marker and reads the archive throughout: status, ticket and dates, each repo with “worktree removed when the lane finished”, the references that were attached, the resume note and the log. A lane id with characters outside letters, digits, ., _ and - just reports “no lane”
lane boardthe same thing in a browser; see The board
lane find <text> [--all]which lane touched this? Searches goals, resume notes, findings, branch names, commit subjects and the files each branch changed. Read-only. --all lists finished lanes too
lane audit [repo]writes .repolane/registry/branch-audit.md: every local branch with no upstream or with unpushed commits
lane sessions <repo|id> [n]past Claude conversations, newest first
lane menuthe long-form menu (needs the long-form names on your PATH)

Reaching into a repo

You never cd into one. These work from wherever you are:

lane run [--lane <id>] <repo> <cmd…>run a command inside one repo of the lane. --lane is needed from the workspace root. The repo may be its short alias from .repolane/registry/repos.yaml
lane gh <repo> <args…>the GitHub CLI, scoped to one repo
lane ref <id> add <path> ["note"] [--rw]attach outside code or docs as a reference (read-only unless --rw). In a Claude session the guard refuses /, your home directory, system and credential directories and secret files
lane ref <id> rm <path|name>, lane ref <id> listdetach one (the target is never touched); show them

lane run and lane gh are a shortcut, not a bypass: whatever they carry is judged by exactly the same rules as a command you typed yourself. Commands run this way inherit WS_HOME, NX_CACHE_DIRECTORY and NX_DAEMON=false from .repolane/env.sh.

Memory and knowledge

lane note is the only way memories get written; its long form is lane-memory, which is what a session inside a lane uses.

lane note draft <slug> ["<text>"|-]write a draft into the inbox (.repolane/cache/memory-inbox/), from the argument or stdin, and print its path; with no text, print the path to write it at
lane note goal [<id>] "<text>"|-replace a lane’s ## Goal: the lane you are in (another lane’s is refused), or <id> from the workspace root. How a lane session saves its Goal, since Claude Code refuses Edit and Write outside the lane folder
lane note show <slug>, lane note list, lane note drop <slug>show a pending draft; list drafts; discard one
lane note file <slug> pref|cross|repo <repo>|lane [<id>]file the draft at that scope and write its index line
lane note move <name> cross|repo <repo>re-scope an existing memory, reindexing both sides
lane note rm <name> [repo <repo>]delete a memory and its index line
lane note index [repo|cross]rebuild the MEMORY.md index lines from the files
lane note auditmemories whose scope looks wrong

The procedure (draft, ask which scope, file) is a convention a session is told to follow; the guard blocks writing the folders directly, and only lane-memory list|show|audit|draft|goal are pre-approved (file, move, rm, drop and index prompt), but nothing makes a session ask you which scope. Every name it is given (slug, repo, lane id, memory name) must be [A-Za-z0-9._-]+. See Rules and Concepts.

Housekeeping

lane merge <a> <b> [--new-branch <name>] [--as <new-id>] [--run]two pieces of work turned out to be one. Prints the plan and changes nothing without --run, and --run must be run from the workspace root because worktrees move
lane plan <a> <b> [--new-branch <name>] [--as <new-id>]the read-only twin of lane merge; --run is rejected
lane doctor [--fix] [--yes] [-v|-q]check the workspace is wired and the safety rules are actually working. --fix repairs what it safely can, asking before it edits CLAUDE.md or anything under ~/.claude (--yes answers). -v lists every location and runs the full guard suite; -q is one line. Exits non-zero if the workspace, the guard or its wiring is broken. The suite’s own result under -v does not change the exit code, so a failing case shows in the output but still exits 0
lane nested [--fix]find worktrees that sit inside another worktree; --fix removes them (never the branch, never one with uncommitted work)
lane workspacerewrite the editor workspace files
lane env [<id-or-repo>]check each worktree has the env files its base clone has, as links. Names only
lane keys [<path|dir>]list the variable names in an env file (default ./.env), never the values. Use it from your own terminal: inside a Claude session the guard refuses this spelling, and only the bare lane-env-keys passes; see Rules
lane secrets <repo> scan|add|ignore|list|ignored [<path-or-glob>…]a repo’s own secret filenames; add writes deny rules. See Extending
lane sync [repo]check a repo’s real GitHub branch protection (gh api) and record it for the guard. Exits 0 only when at least one repo was read from GitHub and none failed; otherwise 1 (see exit codes)
lane upgrade [--check] [--refresh-only] [--drop-missing] [--yes]upgrade the package the way it was installed (pipx upgrade repolane, uv tool upgrade repolane, or for a pip install the pip command to run), then refresh every workspace registered in ~/.local/share/lane/planes.json: the rule files and data migrations, Repolane’s hooks in .claude/settings.json (your own keys and hooks kept), and every lane’s settings and CLAUDE.md. Nothing you wrote is changed. --check shows the installed version against the newest on PyPI and which workspaces are behind, and changes nothing; --refresh-only skips the package step. See Upgrading
lane rules pull [<url>|--latest], lane rules statuspull a shared, SHA-pinned rules source. It never overwrites a same-named file you put in .repolane/scripts/local/: it keeps yours and says so, unless an earlier pull put that file there (tracked in .repolane/cache/scripts-pulled). See Extending

Any executable file you drop into .repolane/scripts/local/ also becomes a lane <name> command of its own, with nothing in the package to edit. It is part of your workspace, a full peer of the built-in commands, and lane help lists it under “your commands:”. Built-ins always win: if a local script shares a name with a real command, the built-in runs and lane doctor flags the local file as shadowed.

lane doctor is the one to run when something feels wrong. It starts with the workspace: lane on PATH the way the hooks call it (if not, every hook fails and Claude Code runs tools unguarded), the import line in CLAUDE.md, the lane hook commands in .claude/settings.json and in every lane, the rule files at the installed version, the registration in planes.json, the memory links and the status line, each problem with its fix. Then it compiles the guard and fires its named probes at it (eleven built in, plus one per rule you added under .repolane/registry/rules.yaml), and checks every lane and worktree is still wired to it. It also prints advisories that never change its exit code: stale lanes (stale_days); GitHub repos whose branch protection lane sync has not checked lately; unreviewed candidate secret filenames; whether gh is installed and signed in; lanes whose spec and worktrees disagree; .repolane/scripts/local/ scripts shadowed by a built-in; and, first and most prominently, whether a lane rules pull source has drifted from its pinned commit. It fails, and names the place, when a lane’s or the root’s PreToolUse matcher lacks Glob|Grep (run lane resume <id>). Only a genuinely broken rule or guard fails the command.

Exit codes

Measured against the product, not promised by it. The product’s tests/ suite asserts the codes for lane sync, lane import --list and bare lane outside a workspace. Convention: 0 means it did what you asked, 1 means it refused or failed, and the dispatcher returns 2 for a command it does not recognise.

You runExit code
lane nosuch2
lane start with no arguments, lane brief <no such lane>, lane park <id> "short" (a note under 10 characters)1
lane done <id> while anything is dirty, unpushed, or holds a secret file the deletion would destroy1
lane merge / lane plan while either lane has uncommitted work1
lane sync with gh missing or not signed in, with no repos, with any repo failing, or when nothing could be checked (every repo local-only or not on GitHub); lane import --list with nothing to find1
lane sync when at least one repo was read from GitHub and none failed0
lane doctor with a healthy guard, with or without advisories (a missing gh is an advisory)0
lane doctor when the guard does not compile, a probe answers wrongly, a rule does not refuse its example, or a lane’s settings do not point at the guard (!! … NOT in force everywhere)1
lane plan <a> <b> --run (it never runs a merge; the message tells you to use lane merge … --run)2
any command other than lane init/migrate/help/--version, run outside a workspace, including bare lane2 (“this folder is not a workspace yet”)

The guard hook has its own codes (How it fails). Scripts that wrap lane should check the code, not parse the words.

Merging, importing and syncing in practice

lane merge and lane plan. Both refuse, with exit 1 and nothing changed, while either lane has uncommitted work (“commit it or lane-park first”). By default b keeps its branches, a’s commits are merged into them and a is archived. --new-branch <name> puts every repo on one new branch carrying both, leaving the original branches as they were; --as <id> makes the result a new lane and archives both. A repo in both lanes is merged; a repo in only one has its worktree moved. The goal, resume note and findings are appended under dated headings and references are unioned. Branches are never deleted, and a conflict stops the merge and leaves the worktree for you.

lane import. It matters only if you want claude --resume on an old conversation from inside a lane. Picking a conversation copies the transcript into the lane’s folder and leaves the original where it was. With nothing under ~/.claude/projects it says so, does nothing and exits 1.

lane sync. Manual; nothing runs it for you. It records in .repolane/registry/repos.yaml (protected_branches: and protection_synced:) which branches GitHub says are protected, and the guard then refuses pushes to those branches as well as the names it already guesses. lane doctor reminds you when the record is old.

What a healthy lane doctor looks like

From a real run in a scratch workspace with one lane and one worktree. gh was not signed in, which is why the advisory is there. It does not fail the command:

$ lane doctor
Workspace ~/one:
  ok    `lane` on PATH runs this workspace's hooks (~/.local/bin/lane, version 0.10.0)
  ok    CLAUDE.md imports .repolane/REPOLANE.md
  ok    .claude/settings.json runs Repolane's lane hook commands
  ok    .repolane/REPOLANE.md and LANE.md are version 0.10.0's
  ok    registered in ~/.local/share/lane/planes.json
  ok    every lane's settings.local.json runs the current hooks (2 file(s))
  ok    Claude Code's memory is linked into .repolane/memory (2 folder(s) under ~/.claude/projects)
  ok    status line: lane statusline

guard.py compiles; 11/11 probes answered as they should:
  deny   a worktree with no lane named
  deny   a worktree opened by path
  deny   reading a .env
  deny   a shell that prints a .env
  deny   writing a memory directly
  deny   a push to a default branch
  deny   rewriting the git remote
  deny   a push to main, written differently
  deny   grepping a .env (Grep reaches it)
  deny   deleting a GitHub repo
  pass   ordinary work is untouched
3/3 places point at guard.py: 1 lane(s), 1 worktree(s), the root

gh is not authenticated — some commands (lane sync, lane gh) will not work until: gh auth login
hundreds more cases behind this (the whole suite):  lane doctor -v

The probes include a push to main written as git push origin 'main'; and a Grep over a .env.

lane doctor -v adds one ok line per location, then runs the whole suite and ends with a line like the one below (the number of cases grows as cases are added):

  1884/1884 cases passed
  ALL PASS

A line that begins !! is the one to act on; see Troubleshooting.

Environment variables

WS_BOARD_PORTthe board’s starting port (default 7777)
WS_BOARD_NO_OPENany non-empty value: start the board without opening a browser
WS_BOARD_REFRESHseconds between the board’s git-state refreshes (default 3)
LANE_WORKSPACEthe workspace to act on, instead of finding it by looking up from the current folder
CLAUDE_HOMEwhere Claude Code keeps its data (default ~/.claude). Read by lane init and lane import. lane add reads it only to look for past conversations; its memory link, lane done, lane sessions, the board and the SessionEnd hook use ~/.claude regardless, so set it consistently or not at all
LANE_PLAINany non-empty value: the session-start script prints plain text instead of the JSON a hook returns. A terminal wrapper uses it; you normally do not
CLAUDE_PROJECT_DIRwhen set (Claude Code sets it for hooks), lane hook and lane statusline find the workspace from it instead of the current directory, because a cd in a session moves the current directory

Not user settings: WS_HOME, NX_CACHE_DIRECTORY and NX_DAEMON are set by .repolane/env.sh for the lane scripts, and LANE_PRODUCT names the installed package’s files for your own commands. ALLOW_DEFAULT_BRANCH_PUSH=1 is not an environment variable but a marker typed at the start of a command; see Rules.

Things worth knowing

lane start writes an editor workspace. Alongside the worktrees you get lanes/<id>/<id>.code-workspace, a multi-root workspace with every repo of the lane, each on the right branch. Open it and the whole ticket is in one window:

{ "folders": [
    { "name": "lane",                    "path": "../.." },
    { "name": "PAY-4182 · api",          "path": "api" },
    { "name": "PAY-4182 · billing-core", "path": "billing-core" },
    { "name": "PAY-4182 · worker",       "path": "worker" }
  ],
  "settings": { "files.exclude": { "repos": true, "lanes": true, ".cache": true, "…": true } }
}

(Abridged: the real settings also carry search excludes and git-detection options.) The first folder, lane, is the workspace root. lane workspace rewrites these files, and also writes a root-level lane.code-workspace covering every active lane (there the lane folder’s path is . and each repo’s path is lanes/<id>/<repo>). More on The board.

lane run accepts short names. Give a repo a short: in .repolane/registry/repos.yaml and lane run api mvn -q package works as well as the full name, case-insensitively.

lane note audit finds memories filed at the wrong scope: written about one repo but phrased as true everywhere.

The board has a JSON API. GET /api/board returns every lane with per-repo ahead, behind, base_behind, staged, unstaged, pr_state and pr_failing. Every call needs the X-Token header. The board does not print it: load / and read it from the page (window.__T), with a Host of 127.0.0.1:<port> or localhost:<port> (anything else gets 403); see The board. The routes are listed in scripts/board/README.md in the Repolane repo.

Long-form names

Most commands also exist as an executable of their own (lane-start, lane-run, lane-memory, …): 27 of them, which the package installs next to lane on your PATH (lane-env-keys and lane-env-check map to lane keys and lane env). The names do not always match the command: lane note is lane-memory, lane plan is lane-merge-plan, lane doctor is guard-check, and lane-help, lane-init, lane-status and lane-sessions exist as well. secrets, sync, rules, upgrade and migrate have no long form. The guard messages and the agent prompts mostly tell you to type the long forms, and they run from any directory without a cd.

A command that behaves differently from this page is a bug in the page or the tool; see Contributing and reporting.

Next steps