repolane v0.11.0 · open source
Open source/Apache 2.0/1,800+ guard test cases

One ticket. Every repo it touches.
One task-scoped Claude Code session.

A feature spans your frontend, API and worker. Repolane gives that work one lane: coordinated Git worktrees, shared task context, and tested Claude Code hooks (scripts Claude Code runs before a tool call) that deny a defined set of risky ones.

Repolane is built to work with Claude Code across several repos. Its command, lane, is the control surface for starting, parking, resuming and checking lanes, and it also works on its own, without Claude.

A lane is one folder holding a Git worktree of every repo a ticket touches, plus a goal and a resume note. Lanes live in a workspace: any folder where you ran lane init, with your CLAUDE.md and lanes/ at the top and everything Repolane keeps under .repolane/.

$ pipx install repolane
Read Architecture ↓
  • NOTHING ALWAYS ONNo hosted service or daemon; the board runs only while you start it
  • PYTHON, BASH & GITOne package, no dependencies: no Docker, no Node requirement
  • HOOK-BASED CHECKSPreToolUse inspection of the tool calls it covers
  • RESUMABLE CONTEXTlane brief reads the lane back

Alone, or with Claude Code

What lane does by itself, and what Claude adds

Every lane command runs without Claude Code. Claude Code is what turns the same lanes into something an agent can walk into: it is driven by hooks that run at session start, on each prompt and before each tool call.

lane on its ownNO CLAUDE NEEDED

Worktrees

lane start makes a branch and a worktree in each repo you name, grouped as one lane.

Notes

lane park WIP-commits what is dirty (leaving secret-named files out) and records a resume note; lane brief prints it back.

Status

lane status shows repos, lanes and drift. lane done checks every repo before it removes anything, and names any secret-named file the removal would delete.

Board

lane board serves a local page of changes, diffs and history, bound to 127.0.0.1, while you have it running.

All of this works in a plain terminal.
With Claude Code drivingADDS

A dispatcher briefing

At the root, Claude receives the board of lanes and a few rules at session start, then enters a lane by id with EnterWorktree.

Hook guardrails

A PreToolUse hook denies a defined set of tool calls and says what to do instead. A guardrail, not a sandbox.

A STATE block on each prompt

Inside a lane, every prompt carries the branch, ahead, behind and uncommitted counts of every repo in it.

Resumable notes

A session opened in a parked lane starts with its status and resume note already in context.

Needs Claude Code: the hooks only see its tool calls. See the recorded sessions.

How a lane is laid out

One lane, a worktree per repo, one guard

Illustrative diagram, built from the demo lane in the terminal output further down. The mechanics are real: base clones in .repolane/repos/, a worktree per repo under lanes/, and the hook boundary.

PAY-4182 Atomic settlement posting and drift reporting
Active (2 dirty, 1 clean)

Settlement batches post to the ledger as one transaction; the nightly job reports drift per account.

Write path committed in all three repos; refunds spanning two batches still need a single transaction (api).

One Git worktree per repo, sharing objects with the base clone in .repolane/repos/
worker/dirty (1)

Drift gauge and per-account logging in the reconcile job.

unpushed 1branch: PAY-4182
api/dirty (2)

Partial refunds as new ledger entries; account index migration.

unpushed 1branch: PAY-4182
billing-core/clean

Drift type and balance recompute for the nightly reconcile.

unpushed 1branch: PAY-4182
Claude Code boundary guard · PreToolUse hook dispatcher

The agent works from the lane root. A PreToolUse hook inspects Bash, Read, Edit, Write, MultiEdit, NotebookEdit, EnterWorktree, Glob and Grep calls before they run. Glob lists names only, so it gets the path rules but no secret check.

9 tool types matched
BLOCKED
Read api/.env

The filename matches the secret-file patterns, so the hook denies the call.

BLOCKED
Grep "TODO" api/

When api/ holds a .env, a search needs a glob that leaves it out ("!.env*") or one that cannot match it ("*.py").

ALLOWED
Edit api/src/services/ledger.ts

An ordinary edit inside the lane's own worktree: the guard answers allow, so Claude Code does not prompt.

Read the full architecture — mirrors, lifecycle, hooks and memory →


Recorded with Claude Code

What Claude actually sees, from the root to a lane

Five stages of one recorded run, in Claude Code's own terms: what the hooks hand Claude, how it walks into a lane, how one session works two repos, what the guard says when it refuses, and what survives a park. Claude's steps were recorded headless on Claude Code 2.1.299 with two small repos; the hook text, command output and refusals were captured again from a real workspace in the same state. Text is quoted as captured; the scratch directory's prefix is shown as … and trimmed output is marked […].

  1. 01

    At the root: the board and the rules

    Start Claude Code at the workspace root and, before your first prompt, a SessionStart hook hands it the owner's name, the board of lanes and a short set of dispatcher rules. A second hook adds one line to every prompt typed at the root.

    SessionStart hook → Claude’s context
    1. Injected by the SessionStart hook (excerpt)
    2. ## lane dispatcher — board at 2026-10-22 22:47 Owner of this workspace: Trial User (REPOLANE.md calls them "the owner"). - **CHG-2** [active, updated 2026-10-22] · api/CHG-2 (0 dirty, 0 unpushed) · web/CHG-2 (0 dirty, 0 unpushed) (written by lane-park: where things stand, next step, blockers) Repos: api web Commands: lane and the lane-* commands are on PATH; call them by name, here and after EnterWorktree (e.g. lane-brief <id>). Workspace data: ~/trial/.repolane (registry, memory, knowledge, mirrors). The rules: ~/trial/.repolane/REPOLANE.md, and LANE.md inside a lane. Dispatcher instructions (details in .repolane/REPOLANE.md "Dispatcher mode"): […] - Fresh session: list the active lanes above, one line each, and ask which one or what new work to start. - Resume: lane-resume <id>, then EnterWorktree name="<lane-id>". New: lane-start <id> <repo>…, then EnterWorktree name="<lane-id>". A name is the only way in (no path, no repo); you land at the lane root and reach each repo at <repo>/… . […] - Leave: lane-park <id> "<note>" then ExitWorktree keep. […] Rules: work only in lanes/, mirrors in .repolane/repos/ stay clean, park before switching, nothing pushed or deleted unasked. See docs/rules.md for why each one exists.
    3. Added to each prompt typed at the root by the UserPromptSubmit hook
    4. [lane] at the dispatcher root, no lane entered. If this request is code work on a repo, first offer/enter a lane (EnterWorktree) and say so; do not edit code from here.
    Claude Code 2.1.299, recorded 2026-10-16; hook text captured 2026-10-22. Message wording can differ from what you see today; run it to see the current text. The board lines are generated from the lane files. The rules are instructions Claude reads, not something the hook enforces by itself.
  2. 02

    Entering a lane by id

    Claude runs the resume script, then calls EnterWorktree with the lane id as the name. The session lands at the lane root, with every repo of the lane beside it.

    root session → lanes/CHG-2
    1. Resuming lane CHG-2 now. This recreates its worktrees and prints its state.
    2. Bash(lane-resume CHG-2)
    3. […] --- api [CHG-2] 0 dirty, 0 unpushed 28bddcc init --- web [CHG-2] 0 dirty, 0 unpushed 65423ee init next: cd …/lanes/CHG-2 && claude
    4. EnterWorktree({"name": "CHG-2"})
    5. Created worktree at …/lanes/CHG-2. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted.
    6. Operator note: I entered the lane worktree at `lanes/CHG-2` with `EnterWorktree`. Next I’ll pull the lane brief, which is read-only.
    7. Bash(lane-brief CHG-2)
    8. ━━ CHG-2 [active] ticket: updated: 2026-10-22 GOAL (one paragraph: what done looks like) RESUME NOTE (written by lane-park: where things stand, next step, blockers) REPO api @ CHG-2 worktree: …/lanes/CHG-2/api dirty: 0 unpushed: 0 […]
    Claude Code 2.1.299, recorded 2026-10-16; hook text captured 2026-10-22. Message wording can differ from what you see today; run it to see the current text. After EnterWorktree the session sits in the lane, and the PostToolUse hook hands it the lane rules (.repolane/LANE.md). The lane commands are the long names the package puts on PATH, so they run from the lane root without a cd.
  3. 03

    One session, two repos

    Inside the lane, api and web sit side by side. Claude edits both from the same session using absolute paths for files and git -C for git, and never leaves the lane root.

    lanes/CHG-2 · one session
    1. Read(…/lanes/CHG-2/api/src/server.py)
    2. 1 def handler(): 2 # TODO: add auth 3 return {"ok": True} 4
    3. Write(…/lanes/CHG-2/web/src/footer.js)
    4. File created successfully at: …/lanes/CHG-2/web/src/footer.js (file state is current in your context — no need to Read it back)
    5. Edit(…/lanes/CHG-2/api/src/server.py)
    6. The file …/lanes/CHG-2/api/src/server.py has been updated successfully. (file state is current in your context — no need to Read it back)
    7. Bash(git -C …/lanes/CHG-2/api diff)
    8. diff --git a/src/server.py b/src/server.py index b956c8e..7a46db3 100644 --- a/src/server.py +++ b/src/server.py @@ -1,3 +1,3 @@ def handler(): - # TODO: add auth + # TODO: add auth (done) return {"ok": True}
    9. Bash(git -C …/lanes/CHG-2/web status)
    10. On branch CHG-2 Your branch is up to date with 'origin/main'. Untracked files: (use "git add <file>..." to include in what will be committed) src/footer.js nothing added to commit but untracked files present (use "git add" to track)
    Claude Code 2.1.299, recorded 2026-10-16; hook text captured 2026-10-22. Message wording can differ from what you see today; run it to see the current text. The per-prompt text inside a lane says not to cd into a repo, and the hook refuses a cd that would leave the lane (the first refusal is in the next stage). The branch is CHG-2 in both repos: one lane, one branch name.
  4. 04

    The guard, refusing in a live session

    Tool calls pass through a PreToolUse hook before they run. These are four refusals, with the exact text Claude got back. The last three came from a self-test Claude was asked to run.

    PreToolUse hook → refusals
    1. Bash(cd …/lanes/CHG-2/api && git status)
    2. PreToolUse:Bash hook error: Stay in the lane — do not cd into api. Anything that must run inside a repo runs there without moving the session: a command: lane-run api <cmd…> (mvn, yarn, tests, scripts) GitHub: lane-gh api <args…> (pr create, pr checks, workflow run) git: git -C api … files: api/path/to/file (edit and read them directly)
    3. Bash(cat api/.env)
    4. PreToolUse:Bash hook error: Secret file. Claude never reads .env/credential files here, even with permission — the read itself is the problem. For structure use `lane-env-keys <path>` (names only) or .env.example; for behaviour, run the app and read its output. (command references api/.env)
    5. Bash(git -C api push origin main)
    6. PreToolUse:Bash hook error: That pushes to a protected branch (develop, main, master). Ask Trial User first. If they agree, run it with the marker they can see: ALLOW_DEFAULT_BRANCH_PUSH=1 git push …
    7. Bash(lane done OTHER-2)
    8. PreToolUse:Bash hook error: `lane done` targets lane 'OTHER-2' but you are in 'CHG-2'. Lifecycle commands only act on this session's own lane; the dispatcher at the workspace root handles the others.
    Claude Code 2.1.299, recorded 2026-10-16; hook text captured 2026-10-22. Message wording can differ from what you see today; run it to see the current text. “Trial User” is the owner name set in the scratch workspace. Each refusal names the way forward, and Claude retried the cd with git -C or lane-run every time. This is a guardrail, not a sandbox: see the security model below for what the hook does not cover.
  5. 05

    Parking, and coming back

    lane park commits work in progress and saves a resume note. The next session, in the lane or at the root, starts with that note already in Claude’s context.

    lane park → next session
    1. Bash(lane-park CHG-2 "api auth stub done; web footer pending")
    2. wip commit in api left out of the commit (secret file names, still on disk): .env.local id_rsa wip commit in web parked CHG-2
    3. Next session, opened in the lane: SessionStart context (excerpt)
    4. ## lane — session in lane 'CHG-2' (CHG-2) --- id: CHG-2 status: parked ticket: […] ## Resume note _2026-10-22_: api auth stub done; web footer pending […] Scope: only lanes/CHG-2/. Park with lane-park CHG-2 "<note>" before they switch away. Lane commands (lane-brief, lane-park, lane-memory, …) are on PATH: call them by name, e.g. lane-brief CHG-2.
    5. Or opened at the root: the board carries the note (excerpt)
    6. ## lane dispatcher — board at 2026-10-22 22:47 Owner of this workspace: Trial User (REPOLANE.md calls them "the owner"). - **CHG-2** [parked, updated 2026-10-22] · api/CHG-2 (2 dirty, 1 unpushed) · web/CHG-2 (0 dirty, 1 unpushed) _2026-10-22_: api auth stub done; web footer pending - **OTHER-2** [active, updated 2026-10-22] · web/OTHER-2 (0 dirty, 0 unpushed) (written by lane-park: where things stand, next step, blockers)
    7. Two lanes exist. **CHG-2** is parked: the api auth stub is done, the web footer is still pending, and api has 2 uncommitted files and 1 unpushed commit. **OTHER-2** is active on web, clean with nothing unpushed.
    Claude Code 2.1.299, recorded 2026-10-16; hook text captured 2026-10-22. Message wording can differ from what you see today; run it to see the current text. Files whose names look like secrets (here .env.local and id_rsa) are left out of the WIP commit and stay on disk. lane done names such files, says to move them into .repolane/repos// or rerun with --discard-ignored, and does not count them as dirty.

The guard is a guardrail, not a sandbox. A tool call it cannot make sense of, or an internal error while judging one, is denied; other hook events fail open. The security model below says exactly where it stops. The same lifecycle, step by step, is in Working with Claude.


Why this exists

Working across repos without a lane, and with one

Why coding agents lose the thread when forced into a single-repo checkout, and what a task-scoped lane changes about that.

Without a laneNO LANE

Ad-hoc clone multiplication

Four copies of the same repo in varying states — ~/src/api-1, ~/src/api-refactor. Stashed diffs get lost and disk space evaporates.

Single-repo blind spots

Claude Code opened inside api/ works from that repo's directory. The client code that consumes your payloads lives next door, and it guesses at contracts it should be reading.

Secret ingestion hazards

An agent with an unrestricted shell can read .env files or certificates, putting credentials into a session transcript.

Mental state evaporation

Context is gone after a week away — uncommitted WIP with no note on what was still failing.

Consequence: duplicated work, broken contracts, a real path to leaked credentials.
With a laneREPOLANE

Base clones, per-ticket trees

One clone per repo stays in .repolane/repos/, never worked in directly. A lane adds a worktree that shares its objects: no re-clone, though the checked-out files still take disk.

One root above every repo

Claude opens at the lane root, above every repo, with the goal and your last resume note already in its context.

Hook-enforced guardrails

A PreToolUse hook denies reads and edits of files whose names match common secret patterns. A push to a protected branch (develop, main or master) needs an ALLOW_DEFAULT_BRANCH_PUSH=1 marker in the command: a visible flag in the transcript, not a permission check.

A note, not just a branch

lane park WIP-commits anything dirty, untracked files included but secret-named ones (such as .env), and records your resume note. lane brief prints it back, any time later.

Outcome: a session scoped to one task, a hook you can test, and a thread of rationale that survives time away.

Workflow

Start, work, park, resume

The everyday sequence, from a fresh ticket to coming back after a week away.

01

Start a coordinated lane

A branch and worktree from each named repo's default branch, grouped as one lane, with an editor workspace file written for the set.

$lane start PAY-4182 api worker billing-core
02

Drive with Claude Code

Run Claude from the lane root, above every repo. The guard hook evaluates the tool calls it covers before they execute.

$cd lanes/PAY-4182 && claude
03

Park, then pick back up

lane park takes a resume note (10+ characters) and WIP-commits what is dirty. lane resume recreates any missing worktrees and prints the note; lane brief is the read-only view.

$lane park PAY-4182 "api left: refunds across two batches"

What it does

Four mechanisms, each with a command

Subsystem 01

Task-scoped Git worktrees

Each repo is cloned once into .repolane/repos/. A lane adds a worktree and a branch named after the ticket in every repo it touches, so two tickets never share a working directory.

Subsystem 02

Shelf-life memory

Four real scopes — pref, cross, repo, lane — decide how long a memory stays true, instead of one markdown file that only grows.

Subsystem 03

A finish line that checks

lane done inspects every repo before removing anything. It refuses while a worktree is dirty, while commits exist that no origin/* branch has, or while deleting the worktree would destroy a secret-named file (--discard-ignored overrides that last one).

Subsystem 04

A local board, on demand

lane board starts a standard-library Python server bound to 127.0.0.1 for status, diffs and commits across a lane. It runs only while you have started it.


Real output

lane brief output

Output of lane brief on a demo lane (a fictional three-repo ledger project), with the username scrubbed from paths and long lines cut by the command itself. The diagram above is drawn from this same lane.

$ lane brief PAY-4182
━━ PAY-4182  [active]  ticket: PAY-4182  updated: 2026-10-22
GOAL
  Settlement batches post to the ledger as one atomic transaction, and the nightly job reports drift
  per account. Done when a partially-failed batch leaves no half-posted entries, refunds post as new
  entries rather than edits, and drift is visible as a metric.
RESUME NOTE
  _2026-10-22_: the write path is committed in all three repos. Left: refunds that span two batches
  need a single transaction (api), and paging on sustained drift (worker). The new index is written
  but not applied — it must run CONCURRENTLY, so it goes out with the infra change, not this deploy.
REPO worker @ PAY-4182
  worktree: ~/work/lanes/PAY-4182/worker
  dirty: 1   unpushed: 1
  1a229e2 Emit a gauge for per-account drift
  e0dbf4c Log drift per account rather than in aggregate
  ec1c73c Raise the visibility timeout to 300s
  8e8c810 Initial commit
   M src/jobs/reconcile.py
  memory index (~/work/.repolane/memory/worker/MEMORY.md):
    - [Queues need 300s visibility](queues-need-300s-visibility.md) — Any queue the reconcile job uses needs a visibility timeout of at least 30
REPO billing-core @ PAY-4182
  worktree: ~/work/lanes/PAY-4182/billing-core
  dirty: 0   unpushed: 1
  822cb6e Add Drift for the nightly reconcile
  edc6705 Recompute balance instead of caching it
  5cb4e68 Initial commit
  memory index (~/work/.repolane/memory/billing-core/MEMORY.md):
    - [Ledger is append only](ledger-is-append-only.md) — The ledger is append-only. Corrections are new entries, never edits or deletes.
REPO api @ PAY-4182
  worktree: ~/work/lanes/PAY-4182/api
  dirty: 2   unpushed: 1
  ad4886c Post partial refunds as new ledger entries
  b1d534d Index ledger entries by account
  3248c88 Post a settlement batch in one transaction
  aa8b033 Key the rate limiter on tenant, not API key
  3036592 Initial commit
   M src/services/ledger.ts
  ?? src/db/migrations/
  memory index (~/work/.repolane/memory/api/MEMORY.md):
    - [Migrations are a pipeline step](migrations-are-a-pipeline-step.md) — Schema migrations run as their own pipeline step before the deploy, 
    - [Rate limits key on tenant](rate-limits-key-on-tenant.md) — The public API rate limit keys on tenant, never on API key.
FINDINGS (live only — archived when this lane finishes)
  - 2026-10-22 — the ledger index has to be created CONCURRENTLY
  - 2026-10-22 — settlement totals are integer cents in api but Money in billing-core
LOG (last 5)
  - 2026-10-22 started
  - 2026-10-22 parked: the write path is committed in all three repos. Left: refunds that span two batches

The full lifecycle this comes from — starting, working across repos, and parking for later — is recorded step by step in Working with Claude.


Security model

Tool-call inspection, not an OS sandbox

The hook inspects Bash, Read, Edit, Write, MultiEdit, NotebookEdit, EnterWorktree, Glob and Grep calls before they run (nine tool types) and denies a defined set of them with an explanation. Glob only lists names, so it gets the path rules but not the secret check. WebFetch, MCP tools and the Task/Agent call itself are not in the matcher, so the hook never sees them, and the secret rule matches a fixed set of filename shapes. This is a guard against agent carelessness, not an operating-system sandbox: it does not claim to prevent prompt injection. If a PreToolUse input is malformed, or the guard hits an internal error while judging a call, it denies the call; the other hook events (prompt context, compaction, session end, file-edit bookkeeping) fail open. It also cannot see inside a script run by name, and a lane created earlier keeps the tool matcher it was created with until lane start, resume, done, merge or ref refreshes it. For a determined adversary, run the whole thing inside a VM or container and treat the guard as one more layer inside it.

One readable fileThe logic is scripts/hooks/guard.py in the package, standard-library Python
Secret matching by name.env* (not .env.example, .sample, .template, .dist), id_rsa and other id_* keys, .netrc, .npmrc, *.pem, *.key, *.p12, *.pfx, *credentials*.json, service-account*.json, secret(s).json/yaml/yml/toml and a few more
Testablelane doctor -v runs the full suite (over 1,800 cases); plain lane doctor fires eleven named probes

Read the full security model →, including exactly where the mechanism stops.

Use with care. Claude and other AI models can make mistakes: they can edit the wrong file, run the wrong command or misread a rule. Repolane's hooks reduce that risk but cannot remove it, and they are a guardrail, not a sandbox. Review what Claude changes, push only what you have read, and keep credentials you cannot afford to lose out of reach. You are responsible for what runs on your machine and in your repositories. Repolane is provided “as is”, without warranty, under the Apache License 2.0.


Install

Install, then start your first lane

Before you run this: you need git, python3 and bash. The test suites pass on macOS and on Ubuntu 24.04; live Claude Code sessions were run on macOS only. Windows is untried (the README suggests WSL). gh is optional, and so is Claude Code: every lane command works without it, and the hook guardrails only matter once Claude Code is driving. Each repo must be added with lane add before lane start can name it.

  1. 1.pipx install repolane
  2. 2.mkdir ~/work && cd ~/work
  3. 3.lane init
  4. 4.lane add https://github.com/octocat/Hello-World.git hello
  5. 5.lane start TRY-1 hello
  6. 6.cd lanes/TRY-1 && claude

Step 1 is once per machine (uv tool install repolane works too). Any folder where lane init ran is a workspace; it asks for your name, and asks before it edits a CLAUDE.md you already have or links Claude Code's memory under ~/.claude. Steps 4 and 5 use GitHub's public octocat/Hello-World demo repo so they run exactly as written; for real work, swap in your own repo's URL (or a path to a checkout you already have) and repeat lane add for every repo a lane will name. The PAY-4182 lane in the diagram and terminal above is a separate, fictional demo. Set up from a git clone? Move it to a workspace with lane migrate.

Going deeper: Architecture · Security · Troubleshooting · Contributing

Public source, tagged release v0.11.0. Apache 2.0. The scripts contain no telemetry or analytics code (checked for outbound HTTP), but they do use the network when something needs it: git remotes (including a fetch in every base clone each time you run lane status), PyPI when you run lane upgrade, gh if you have it (the board polls it while open). The board binds to 127.0.0.1.