repolane v0.11.0 · open source

Guides

Working with Claude

What a Claude Code session inside a lane is actually told, what it may do without asking, and what it is refused.

On this page

In brief. This page answers what a Claude Code session inside a lane is told, what it may do without asking, and what it is refused. Remember: instructions are requests the model usually follows; hooks are checks that answer before a tool runs. Only the hooks can refuse, and they judge only the tool calls they are shown.

You do not have to type any of the commands in this documentation. They exist so that you can drive when you want to — but in normal use you describe the work and Claude runs the lane itself.

     billing-core@PAY-4182 ahead 0 behind 0 dirty 0; worker@PAY-4182 ahead 1 behind 0 dirty 1;
     touched worker/src/jobs/reconcile.py — UNPARKED: uncommitted work left in a worktree
       - 2026-09-18 08:55 session ended: api@PAY-4182 ahead 1 behind 0 dirty 0;
     billing-core@PAY-4182 ahead 0 behind 0 dirty 0; worker@PAY-4182 ahead 2 behind 0 dirty 0
       - 2026-09-18 08:55 session ended: api@PAY-4182 ahead 1 behind 0 dirty 0;
     billing-core@PAY-4182 ahead 0 behind 0 dirty 0; worker@PAY-4182 ahead 2 behind 0 dirty 0

     commands: ! lane-menu   (prefix any lane-* command with "! " to run it here without a model
     call)

❯ read this lane with lane-brief and tell me in three lines what is left to do

⏺ Skill(/lane-brief)
  ⎿  Successfully loaded skill · 1 tool allowed
  ⎿  Allowed by auto mode classifier

  Ran 3 shell commands

⏺ Ran scripts/lane-brief PAY-4182.

  Left to do, in three lines:
  1. api — refunds that span two batches must post in a single transaction (1 commit unpushed
     there).
  2. worker — paging on sustained drift; src/jobs/reconcile.py is modified and test_reconcile.py is
     untracked, 2 commits unpushed.
  3. Then push api + worker and close out; mobile, web, billing-core are clean and done.

✻ Brewed for 14s · done 10:05 AM

────────────────────────────────────────────────────────────────────────────────────────────────────
❯


  ⏵⏵ auto mode on (shift+tab to cycle)

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

A real session, recorded with asciinema in a lane and replayed here as text, not a video. Press play, or select any line and copy it. Paths and names in it are made up; the username in the paths is fictional. Recorded 2026-09-18 on Claude Code 2.1.276. Message wording can differ from what you see today; run it to see the current text.

Below is the same session written out step by step, for reading rather than watching. The sessions in these docs are real recordings, but the workspace they ran in is a demo one: the owner, ticket ids (SHOP-418, PAY-4182) and repo names are made up, and usernames in paths are fictional. The recorded Claude sessions on this page predate the current workspace layout, so the paths in them read ~/repolane/… and scripts/lane-… where a workspace today has ~/work/.repolane/… and the long command names on PATH; the hook output shown as text further down is current.

~/repolane/lanes/PAY-4182
  1. >Look at this lane, then finish the drift work in worker: emit_drift_metric should skip accounts whose drift is zero, and add a short unit test. Show me the diff before you commit.
  2. I'll read the lane context and the worker code first.
  3. Bash(ls && echo "--- memory worker ---" && cat ~/repolane/memory/worker/MEMORY.md 2>/dev/null && echo "--- prefs ---" && cat ~/repolane/knowledge/preferences.md 2>/dev/null)
  4. api billing-core CLAUDE.md mobile PAY-4182.code-workspace web worker --- memory worker --- # Memory Index — worker … +27 more lines
  5. Bash(git status --short && git log --oneline -5 && echo "--- tree ---" && f)
  6. 4f57d4d Tag the drift gauge with direction 459e554 Emit a gauge for per-account drift 198bf0a Log drift per account rather than in aggregate f0b6b05 Raise the visibility timeout to 300s 897612a Initial commit --- tree --- ./README.md ./.claude/settings.local.json ./src/lib/queue.py … +1 more line
  7. Bash(cat lanes/PAY-4182/worker/src/jobs/reconcile.py)
  8. """Nightly reconciliation: does the ledger agree with what we settled?""" from dataclasses import dataclass from decimal import Decimal from ..lib import billing, api from ..lib.money import Money @dataclass(frozen=True) … +43 more lines
  9. Bash(ls lanes/PAY-4182/worker/src/lib/; cat lanes/PAY-4182/worker/README.md)
  10. queue.py # worker Background reconciliation and webhook delivery.
  11. Bash(python3 - <<'EOF' # edit reconcile.py, then show the diff)
  12. diff --git a/src/jobs/reconcile.py b/src/jobs/reconcile.py index be9734a..18dfb5e 100644 --- a/src/jobs/reconcile.py +++ b/src/jobs/reconcile.py @@ -41,6 +41,9 @@ def drift_direction(result: ReconcileResult) -> str: def emit_drift_metric(result: ReconcileResult) -> None: + """Accounts with no drift are not worth a series; skip them.""" … +27 more lines
  13. Diff is above. Two changes in `worker`:
A real session, recorded with claude -p --output-format stream-json and converted to the steps below from the session's JSON log. Paths shortened; nothing else changed. Claude stopped before the push on its own — the lane rules arrive in its context, so the hook is the backstop, not the first line of defence.

That is a re-enactment: the pacing is staged, but the commands and outputs in it come from the recorded session’s log. The rest of this page shows the raw hook output behind it.

Two things hold the line. The rules in .repolane/REPOLANE.md (which your CLAUDE.md imports), .repolane/LANE.md inside a lane, and the context injected into each prompt are instructions: the model reads them and, usually, follows them. The hooks are a separate mechanism: they read the arguments of a tool call before it runs and answer with a decision, and the model is not the thing being asked. The hooks are a guardrail, not a sandbox: see the security model for how they fail and what they never see. Rules says which rule is which.

Use with care. Claude can make mistakes, and the hooks are a guardrail, not a sandbox. Review what it changes; you are responsible for what runs on your machine. Details.

Two ways to drive, at the same time

Askdescribe the work; Claude starts the lane, edits across repos, runs the tests and proposes the commit
Typelane status, lane brief, lane park — the same operations, in your terminal, at any moment

Claude runs the same lane-* commands you would (the long names the package puts on PATH), and the lane file is plain text either way, so you can alternate mid-task.

Opening a session

cd lanes/PAY-4182 && claude

Before you type anything, the SessionStart hook has already put the lane file in Claude’s context: the repos in scope, the goal, the note you left yourself last time, and the findings that only matter while this work is live. It then names the owner and the lane file, and hands over .repolane/LANE.md, the rules for working inside a lane.

## lane — session in lane 'PAY-4182' (PAY-4182)

---
id: PAY-4182
status: active
ticket: PAY-4182
created: 2026-10-22
updated: 2026-10-22
repos:
  - repo: worker
    branch: PAY-4182
    base: main
  - repo: billing-core
    branch: PAY-4182
    base: main
  - repo: api
    branch: PAY-4182
    base: main
---

## 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.

## Findings
(things learned that matter only while this work is live; archived with it.
 Anything that stays true afterwards belongs in .repolane/memory/<repo>/ instead — lane-memory file <slug> repo <repo>)
- 2026-10-22 — the ledger index has to be created CONCURRENTLY
  The table is 40M rows; a plain CREATE INDEX held a write lock for eleven minutes in staging.
- 2026-10-22 — settlement totals are integer cents in api but Money in billing-core
  Convert at the boundary, never inside the loop — a mid-loop conversion is what caused the August drift.

## Log
- 2026-10-22 started
- 2026-10-22 parked: 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.

Read .repolane/memory/<repo>/MEMORY.md for each repo above, then brief John Doe in five lines and continue the work.
Scope: only lanes/PAY-4182/. Park with lane-park PAY-4182 "<note>" before they switch away.
Lane commands (lane-brief, lane-park, lane-memory, …) are on PATH: call them by name, e.g. lane-brief PAY-4182.

## Repolane: this session is in lane 'PAY-4182'
Owner: John Doe (the rules below and REPOLANE.md call them "the owner").
Lane folder: ~/work/lanes/PAY-4182/  ·  lane file: ~/work/.repolane/registry/lanes/PAY-4182.md (read it; set its Goal with `lane-memory goal "<text>"`, never Edit/Write).
The lane-* commands are on PATH next to `lane`; call them by name. If one is not found, the owner runs `lane doctor` in a terminal: it says what to put on PATH.

# You are inside a lane

This session sits in a lane: `lanes/<id>/` of a Repolane workspace, one piece of work across the repos in it.
The lane's id, its file and the owner's name are given just above this text.

## What you cannot do here, and what to do instead
Claude Code does not let a session in a worktree use Edit or Write on anything outside the lane folder. It refuses
before any hook can help, so do not try. That includes the lane's own file under `.repolane/registry/lanes/` and the
memory folders. Use these commands instead (they run without asking):

- **Set or change the lane's Goal** — when the owner says what this lane is for ("the goal is …", "set the goal to …",
  "this lane is about …"), save it with:
      lane-memory goal "<the goal, in their words>"
  It replaces the `## Goal` section of this lane's file. Long text: `lane-memory goal -` and the text on stdin.
- **Remember something** (a fact, a decision, a preference worth keeping) — write a draft with:
      lane-memory draft <short-slug> "<what to remember>"
  then ask the owner which scope it belongs to (`lane` · `repo <repo>` · `cross` · `pref`) with a recommendation, and
  file it with `lane-memory file <short-slug> <scope>`. Never write memory files yourself.
- **A resume note or a status change** — `lane-park <id> "<note>"` when they say park or switch away.

## Rules inside a lane
1. Stay here. Never `cd` out of the lane; reach a repo with `<repo>/…` paths, `git -C <repo> …`,
   `lane-run <repo> <cmd…>` (builds, tests) and `lane-gh <repo> <args…>` (GitHub). Do not `cd` into a repo.
2. Only the repos in the lane file are in scope. Not the mirrors under `.repolane/repos/`, not other lanes, not
   anything else on the machine. Outside code or docs come in as a reference, once the owner agrees:
   `lane-ref <id> add <path> "why"`, then read it under `lanes/<id>/refs/<name>/`.
3. Never read `.env` or credential files. `lane-env-keys <path>` lists the names in one, never the values.
4. A missing repo is added with `lane-with <id> <repo>`; never create another worktree or lane from here.
5. Report git state only from the STATE block that arrives with each prompt.
6. Discuss before deciding: propose anything with judgment in one line with a recommendation, and wait.

A session opens with the goal, the resume note and the findings already in context, and you do not re-explain them. The hook also says the lane commands (lane-brief, lane-park, lane-memory, …) are on PATH and to call them by name. The transcript above is real output of lane hook session-start fed the event Claude Code sends; message wording can differ from what you see today, so run it to see the current text.

Saving the Goal and notes from inside a lane

Inside a lane, Claude Code refuses Edit and Write on any path outside the lane folder (“This session is isolated in the worktree”), before any hook runs and whatever the settings say. The lane’s own file (.repolane/registry/lanes/<id>.md) and the memory folders are outside it. So a session in a lane writes them through commands, which LANE.md tells it to use and which run without a prompt:

lane-memory goal "<the goal, in the owner's words>"     # replaces the lane file's ## Goal (refuses another lane's)
lane-memory goal -                                       # the same, with the text on stdin
lane-memory draft <slug> "<what to remember>"            # a memory draft in the inbox (or - for stdin)
lane-memory file <slug> lane|repo <repo>|cross|pref      # file it, once you have picked the scope (this one prompts)

Reads of .repolane/memory/ and .repolane/knowledge/ are allowed from every lane. A resume note is lane-park <id> "<note>".

Starting from the workspace root

You can also start claude at the workspace root instead of in a lane. The SessionStart hook then hands Claude a board of every lane (per repo: dirty and unpushed counts) and the parked note under each parked one, plus the dispatcher rules: discuss before deciding, and say every lane-* script it runs. Each prompt at the root adds a reminder not to edit code from there. In a live run (Claude Code 2.1.299, headless) Claude summarised the board and asked which lane to take, as instructed, rather than choosing.

To go into a lane it runs lane-resume <id> and then calls EnterWorktree name="<lane-id>". That is the only form that works. A path= is denied (the message is There is one way into a lane: EnterWorktree name="<lane-id>"), and so is a repo (name="<id>/<repo>"). The injected dispatcher text says name="<lane-id>"; in a live trial (Claude Code 2.1.299, headless) Claude entered on its first try (Created worktree at …/lanes/<id>).

Where it lands: the lane root, lanes/<id>/, not inside a repo. From there one session works on every repo of the lane, using absolute paths for files and git -C <repo> for git. As it enters, the PostToolUse hook hands it .repolane/LANE.md.

Every prompt carries the state

On each message you send, the hook injects the current state of every repo in the lane, and, the first time, the memory index for each of them. Claude does not have to run git status in three places to find out where things stand. The state line is a snapshot taken when you send the message, so it can be behind by whatever happened since.

MEMORY for this session (indexes; open a linked file when it is relevant):
## preferences — ~/work/.repolane/knowledge/preferences.md
# Preferences

How you want to be worked with. Claude reads this at the start of any session that writes code.
Delete what does not apply and add what does.

## Communication

- Plain words. Explain a diagram rather than writing three paragraphs.
- Say what changed and what is still broken. Do not hedge finished work.

## Before acting

- Discuss anything with judgement in it: a new lane's name, scope, or approach.
- Run read-only things freely.

## Commits and branches

- Ask before committing.
- Commits are coherent units, not one per edit and not one giant one.
- Never push to a default branch without being told.

## Code

- Match the surrounding style rather than importing your own.
- Say when a change needs a test, and write it.
## cross-repo — ~/work/.repolane/memory/MEMORY.md
# Memory Index — applies everywhere (per-repo memory lives in the subfolders)
- [Deploys are blue green](deploys-are-blue-green.md) — Every service deploys blue-green; nothing restarts in place, in any environment.
- [Money is integer minor units](money-is-integer-minor-units.md) — Money crosses every service boundary as integer minor units with an explicit currency.
## api — ~/work/.repolane/memory/api/MEMORY.md
# Memory Index — api
- [Migrations are a pipeline step](migrations-are-a-pipeline-step.md) — Schema migrations run as their own pipeline step before the deploy, never on boot.
- [Rate limits key on tenant](rate-limits-key-on-tenant.md) — The public API rate limit keys on tenant, never on API key.
## billing-core — ~/work/.repolane/memory/billing-core/MEMORY.md
# Memory Index — billing-core
- [Ledger is append only](ledger-is-append-only.md) — The ledger is append-only. Corrections are new entries, never edits or deletes.
## worker — ~/work/.repolane/memory/worker/MEMORY.md
# Memory Index — worker
- [Queues need 300s visibility](queues-need-300s-visibility.md) — Any queue the reconcile job uses needs a visibility timeout of at least 300 seconds.
MEMORY SCOPES — where a thing belongs is decided by how long it stays true:
  preference   .repolane/knowledge/preferences.md    forever, all work    "how should Claude work with me?"
  cross-repo   .repolane/memory/<slug>.md            forever, any repo    "true in every repo?"
  repo         .repolane/memory/<repo>/<slug>.md     forever, that repo   "still true after this ticket ships?"
  lane         .repolane/registry/lanes/<id>.md      until it finishes    "only matters while this work is live?"
               (its ## Findings section)                                    then archived with the lane
TO SAVE A MEMORY: never write the file yourself. `lane-memory draft <slug> "<draft>"` (or `-` and a heredoc), ASK John Doe which scope (recommend one), then `lane-memory file <slug> lane|repo <repo>|cross|pref`. The index line is written for you. Findings of this lane are in its spec under ## Findings.

STATE of lane PAY-4182 (every repo in it; * = where you are: lanes/PAY-4182). Report state only in this form:
    api                    branch: PAY-4182                   ahead:   1  behind:   0  uncommitted: 2
    billing-core           branch: PAY-4182                   ahead:   1  behind:   -  uncommitted: 0
    worker                 branch: PAY-4182                   ahead:   1  behind:   0  uncommitted: 1
All 3 repos above are yours to work on right now — this is one piece of work, not one repo. You are in the lane itself, which is the normal place to sit — no repo is home.
  edit any repo:    <repo>/… under lanes/PAY-4182/ (absolute paths work from anywhere in it)
  git in a repo:    git -C <repo> …
  run anything in a repo:  lane-run <repo> <cmd…>   ·   GitHub: lane-gh <repo> <args…>
  do NOT cd into a repo — you never need to, and the hook refuses it
  NEVER ExitWorktree/EnterWorktree to change repo — a cd does it and keeps the lane.
Spec: ~/work/.repolane/registry/lanes/PAY-4182.md. Never mention .repolane/repos/ or the old checkouts.

The block asks Claude to report state “only in this form”, and in a live trial (Claude Code 2.1.299) Claude reproduced the table in its answers. For a lane with more than one repo it adds a reminder that every repo is Claude’s to work on from where it sits: edit <repo>/… by absolute path, git -C <repo> …, lane-run <repo> <cmd…>, and do not cd into a repo. On the numbers: ahead is the count of commits on the branch that no origin/* branch has, the same count lane status, lane done and the board use, so a plain git push origin <branch> (without -u) brings it to 0. uncommitted is the number of changed paths git status reports.

Note what is not here: the memory notes themselves. For repo and cross-repo memory only the index (MEMORY.md) goes in, so a large memory folder cannot eat the context window; Claude opens the files whose one-line description looks relevant. What does go in as text (each cut at 3,000 characters, the indexes included) is .repolane/knowledge/preferences.md and every .md file in .repolane/knowledge/shared/: those are read on the first prompt as written, so treat them as instructions you have chosen to trust.

What it can do without asking

Inside a lane, Claude works across every repo at once without moving the session:

edit any repo<repo>/path/to/file — absolute paths work from anywhere in the lane
git in a repogit -C <repo> …
run anythinglane-run <repo> <cmd…> — tests, builds, package managers
GitHublane-gh <repo> pr create
read statelane-status, lane-brief, lane-find, lane-menu, lane-sessions, lane-merge-plan, lane-env-check, lane-env-keys and lane-memory list|show|audit are on the pre-approved list in a lane’s settings (the workspace’s own settings.json leaves out lane-find)
save the Goal, draft a memorylane-memory goal and lane-memory draft are pre-approved in a lane and at the workspace root (a session that started at the root and entered a lane keeps the root’s settings), because Edit and Write cannot reach the lane file or the memory folders from inside a lane
lane commandsby their long names (lane-brief <id>), which the package puts on PATH and which run from any folder
ask firstthe other lane-memory verbs (file, rm, move, drop, index) and lane-workspace are not pre-approved: they write or delete, so they prompt. lane-memory validates its slug (see the security model)

The pre-approved list is Claude Code’s own permission setting, and it only applies once the workspace has accepted Claude Code’s trust dialog. In a headless run on Claude Code 2.1.299 in a workspace that had not, Claude Code printed “Ignoring N permissions.allow entries from .claude/settings.json: this workspace has not been trusted” and none of it applied. Interactively the dialog comes first; that path was not exercised in the trials.

cd into a repo is denied, by design. The session stays at the lane root: entering a repo makes it look like the main one. The denial names what to do instead (lane-run <repo> <cmd…>, git -C <repo> …, lane-gh, or <repo>/path for files). In a live trial (headless, Claude Code 2.1.299, model haiku) it was the model’s first instinct in six of fifteen tasks, and each denial was recovered in one retry.

lane-run and lane-gh are a shortcut, not a bypass: whatever they carry is judged by exactly the same rules as a command typed directly. The guard judges the command, not what the process it starts goes on to do: a test suite or a build script runs with your account’s access, not the lane’s (see what the guard does not check).

Edits inside the lane are not prompted. For file tools aimed at the lane, the guard answers allow, which short-circuits Claude Code’s own permission prompt. If you use Claude Code’s prompts as a review step for edits, they do not appear for paths inside the lane.

     ahead 0 behind 0 dirty 1 — UNPARKED: uncommitted work left in a worktree
       - 2026-09-18 13:12 session ended: api@SHOP-418 ahead 0 behind 0 dirty 2; mobile@SHOP-418
     ahead 0 behind 0 dirty 1 — UNPARKED: uncommitted work left in a worktree

     commands: ! lane-menu   (prefix any lane-* command with "! " to run it here without a model
     call)

❯ api now returns a priced basket from priceBasket. update mobile's Checkout to render what the api
  returns instead of computing the total locally, then show me the diff in both repos

  Searched for 1 pattern, listed 1 directory, ran 2 shell commands

⏺ Edited mobile/src/Checkout.tsx (no scripts run, no lane file changed). Both diffs are above.

  Checkout now takes one basket prop matching what priceBasket returns (subtotal, discount, total,
  applied) and renders it — the local promo ? subtotal - 5 math is gone.

  Two things to flag:

  - api/src/index.ts is uncommitted work that was already there; I did not touch it. It references
    Item and lookupPromo, neither of which exists in that file — the api side won't compile as-is.
  - Nothing in mobile calls Checkout yet, so no call site needed updating. Whoever wires it will
    have to pass the api response through.

  State: api SHOP-418, ahead 0 behind 0, 2 uncommitted · mobile SHOP-418, ahead 0 behind 0, 1
  uncommitted.

✻ Worked for 15s · done 1:12 PM

────────────────────────────────────────────────────────────────────────────────────────────────────
❯


  ⏵⏵ auto mode on (shift+tab to cycle)

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

Everything above, in one real session. It opens at the lane root with the goal and both repos already in hand, reads api, edits mobile, and diffs both, without once changing directory. It then flags that the pre-existing api edit references two symbols that do not exist, so that side will not compile, and closes with where both repos stand. Paths and names in it are made up. Recorded 2026-09-18 on Claude Code 2.1.277. Message wording can differ from what you see today; run it to see the current text.

What it is refused

These are real refusals from the guard hook, worded as Claude receives them. Most say what to do instead.

        M src/index.ts
       ?? .env
       memory index (/Users/john.mercer/repolane/memory/api/MEMORY.md):
     LOG (last 5)
       - 2026-09-18 started
       - 2026-09-18 11:01 session ended: api@SHOP-418 ahead 0 behind 0 dirty 2; mobile@SHOP-418
     ahead 0 behind 0 dirty 1 — UNPARKED: uncommitted work left in a worktree
       - 2026-09-18 11:03 session ended: api@SHOP-418 ahead 0 behind 0 dirty 2; mobile@SHOP-418
     ahead 0 behind 0 dirty 1 — UNPARKED: uncommitted work left in a worktree
       - 2026-09-18 11:08 session ended: api@SHOP-418 ahead 0 behind 0 dirty 2; mobile@SHOP-418
     ahead 0 behind 0 dirty 1 — UNPARKED: uncommitted work left in a worktree

     commands: ! lane-menu   (prefix any lane-* command with "! " to run it here without a model
     call)

❯ read api/.env and tell me what DATABASE_URL is set to

⏺ I can't read that — rule 8: .env and credential files are off limits, and the hook blocks it.

  What I can get you instead:
  - lane-env-keys api/.env — the key names, no values
  - cat api/.env.example — the documented structure

  Want me to run one of those?

✻ Sautéed for 2s · done 1:11 PM

────────────────────────────────────────────────────────────────────────────────────────────────────
❯


  ⏵⏵ auto mode on (shift+tab to cycle)

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

A real session asked for a value out of .env. The model declines on its own, citing rule 8 from the instructions it was given, before any tool call; the hook's refusal below is what answers if it tries the read. Paths and names in it are made up. Recorded 2026-09-18 on Claude Code 2.1.276. Message wording can differ from what you see today; run it to see the current text.
# Claude tries to read a .env file
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.

# Claude tries to work on a different lane
'PAY-4207' is a different lane. This session is in 'PAY-4182'. Ask John Doe to add that repo with lane-with PAY-4182 <repo>, or switch lanes.

# Claude tries to push to main
That pushes to a protected branch (develop, main, master). Ask John Doe first. If they agree, run it with the marker they can see: ALLOW_DEFAULT_BRANCH_PUSH=1 git push …

The secret-file denial has separate read and write texts, and the recursive-search denial names --exclude='.env*'. What was observed live, against Claude Code 2.1.299 (headless), for the things worth knowing about:

Tried in a real sessionWhat happenedBy
Read of a lane’s api/.envrefused: “File is in a directory that is denied by your permission settings”Claude Code’s own permissions.deny, with no guard output
Bash cat api/.envrefused with the secret-file textthe guard
Read of a file under $HOME outside the workspacerefusedthe guard
Read or Write of another lane’s filerefusedthe guard
git -C api push origin HEAD:mainrefused, with the visible-marker textthe guard
cd /tmp && pwdrefused (“a cd moves the session itself … the way back is EnterWorktree, not cd”)the guard
EnterWorktree path=…refused: one way into a lane, name="<lane-id>"the guard
pushd /tmp, builtin cd /tmprefused by the guard, like a plain cdthe guard

So the .env read has two independent layers. The guard is one; the other is configuration in the lane’s settings that has to be present too (see the security model).

A refusal that asks you a question: memory writes

Claude is not forbidden from remembering things. A session may not decide how long they stay true, which is a question for you. So the hook refuses a direct write to .repolane/memory/ or .repolane/knowledge/ and hands Claude the procedure for asking you instead.

# Claude tries to write a memory itself
Memories are not written directly — the scope is John Doe's decision.
MEMORY SCOPES — where a thing belongs is decided by how long it stays true:
  preference   .repolane/knowledge/preferences.md    forever, all work    "how should Claude work with me?"
  cross-repo   .repolane/memory/<slug>.md            forever, any repo    "true in every repo?"
  repo         .repolane/memory/<repo>/<slug>.md     forever, that repo   "still true after this ticket ships?"
  lane         .repolane/registry/lanes/<id>.md      until it finishes    "only matters while this work is live?"
               (its ## Findings section)                                    then archived with the lane
Do this instead:
  1. lane-memory draft <slug> "<draft>"   (or `-` and a heredoc; do not use Edit/Write, a worktree session cannot)
  2. ASK John Doe in one line: what it says, and which of the four scopes it is. Recommend one, using the
     survival test: does it stay true after this ticket ships? after leaving this repo?
  3. lane-memory file <slug> lane | repo <repo> | cross | pref     (files it and writes the index line)

Whether the session actually asks is an instruction, not a check.

Slash commands

lane init puts Repolane’s slash commands in the workspace’s .claude/commands/, and lane upgrade keeps them current. A file of your own with the same name is never replaced, and neither is one of Repolane’s you edited. Most of them run one command and show its output:

/lane-startpropose an id, create the lane, enter it, draft the goal
/lane-briefthe brief above, verbatim
/lane-parkwrite the resume note and park
/lane-resumerecreate worktrees and print state
/lane-donefinish, refusing if anything is dirty or unpushed
/statusthe status of every repo and lane (lane-status)
/lane-sessionspast conversations for a repo or lane
/lane-helpthe menu of lane commands

Most are a description, one lane-* call and an instruction to show the output verbatim, so /lane-brief returns command output rather than a model’s summary. /lane-start proposes an id, asks “go?”, then runs lane-start and enters the lane.

Subagents

Three agents come with the workspace, in .claude/agents/, for jobs that deserve their own context window. They are installed and kept current the same way as the slash commands.

context-loaderreads the lane file, preferences, per-repo memory, the repo’s own CLAUDE.md and domain notes, and returns a compact brief
lane-managerstarts, resumes, parks and finishes lanes
branch-gardenertriages local branches with read-only git and proposes deletions; never deletes without an approved list

context-loader reads widely in its own window and returns a short brief, so assembling context does not spend the main session’s window.

Subagents and the guard. A subagent’s own Bash, Read, Edit, Write and Grep calls go through the same guard as yours: in live runs (Claude Code 2.1.299, headless) a subagent’s Read and Bash of a file outside the workspace were denied, and so was its Grep over a folder holding a .env. (Edit and Write are in the same matcher but were not separately exercised.) The Agent/Task tool call itself is not matched, and neither are WebFetch or MCP tools. Not tested: a subagent calling Glob, WebFetch or an MCP tool.

What happens when a session ends

When you close the session, the SessionEnd hook appends one line to the lane file’s log: each repo’s branch, how far ahead and behind it is, how many files were dirty, and up to twelve files the edit tools touched (a file changed by a shell command is not listed). If you left uncommitted work behind, the line ends with UNPARKED. That marker is in the log, where lane brief and the lane file show it; lane status does not read it. A line identical to the one before it apart from its timestamp is skipped, so read-only and denied runs do not fill the log with copies.

Parking is how context travels to the next session. lane park writes the dated note into the lane file; a fresh session in that lane receives status: parked and the note, and a session at the workspace root sees the lane on its board with the note under it. In a live run (Claude Code 2.1.299, headless) the next session answered from it (“CHG-1 (parked): the api now returns the plan and the web header shows it …”). lane park never commits a file whose name says secret; it leaves it on disk and prints which, see Rules.

~/work $ lane brief SHOP-412
━━ SHOP-412  [parked]  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 park committing what is dirty as a WIP commit (a file whose name says secret is left on disk, uncommitted) and writing the resume note, so next week's lane brief starts from the same place this session left off.

Checking the rules still hold

A guard that silently stops guarding is worse than no guard. A guard that has stopped running, or a lane whose hooks are gone, refuses nothing, and a rule that stops matching stops refusing without any error. lane doctor is how you notice. It compiles the guard, fires its eleven named probes at it (ten that must be refused and one that must not, since a guard that denies everything is equally broken) and then walks every lane and worktree to check each still points at the guard, because a session whose settings.local.json lost its hooks is unguarded while the guard itself is fine. It also flags a lane whose hook matcher lacks Glob|Grep: a lane created earlier keeps the tool matcher it was created with until lane start, resume, done, merge or ref refreshes it (lane resume <id> does so directly), and until then the Grep tool skips the guard there. Before any of that it checks that lane is on the PATH the hooks use, because every hook is a lane hook … command and a hook that cannot start leaves Claude Code running tools unguarded. It exits non-zero and says which place is wrong. lane doctor -v also runs the full guard test suite. A green doctor means the rules are in force, not that nothing can get past them; the security model lists what does.

Read Rules for the reasoning behind each refusal, and Security model for what the mechanism does and does not protect against.

Next steps