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
- Two ways to drive, at the same time
- Opening a session
- Saving the Goal and notes from inside a lane
- Every prompt carries the state
- What it can do without asking
- What it is refused
- A refusal that asks you a question: memory writes
- Slash commands
- Subagents
- What happens when a session ends
- Checking the rules still hold
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.
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.
- >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.
- I'll read the lane context and the worker code first.
- Bash(ls && echo "--- memory worker ---" && cat ~/repolane/memory/worker/MEMORY.md 2>/dev/null && echo "--- prefs ---" && cat ~/repolane/knowledge/preferences.md 2>/dev/null)
- api billing-core CLAUDE.md mobile PAY-4182.code-workspace web worker --- memory worker --- # Memory Index — worker … +27 more lines
- Bash(git status --short && git log --oneline -5 && echo "--- tree ---" && f)
- 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
- Bash(cat lanes/PAY-4182/worker/src/jobs/reconcile.py)
- """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
- Bash(ls lanes/PAY-4182/worker/src/lib/; cat lanes/PAY-4182/worker/README.md)
- queue.py # worker Background reconciliation and webhook delivery.
- Bash(python3 - <<'EOF' # edit reconcile.py, then show the diff)
- 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
- Diff is above. Two changes in `worker`:
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
| Ask | describe the work; Claude starts the lane, edits across repos, runs the tests and proposes the commit |
| Type | lane 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 repo | git -C <repo> … |
| run anything | lane-run <repo> <cmd…> — tests, builds, package managers |
| GitHub | lane-gh <repo> pr create |
| read state | lane-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 memory | lane-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 commands | by their long names (lane-brief <id>), which the package puts on PATH and which run from any folder |
| ask first | the 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.
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.
.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 session | What happened | By |
|---|---|---|
Read of a lane’s api/.env | refused: “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/.env | refused with the secret-file text | the guard |
Read of a file under $HOME outside the workspace | refused | the guard |
Read or Write of another lane’s file | refused | the guard |
git -C api push origin HEAD:main | refused, with the visible-marker text | the guard |
cd /tmp && pwd | refused (“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 /tmp | refused by the guard, like a plain cd | the 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-start | propose an id, create the lane, enter it, draft the goal |
/lane-brief | the brief above, verbatim |
/lane-park | write the resume note and park |
/lane-resume | recreate worktrees and print state |
/lane-done | finish, refusing if anything is dirty or unpushed |
/status | the status of every repo and lane (lane-status) |
/lane-sessions | past conversations for a repo or lane |
/lane-help | the 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-loader | reads the lane file, preferences, per-repo memory, the repo’s own CLAUDE.md and domain notes, and returns a compact brief |
lane-manager | starts, resumes, parks and finishes lanes |
branch-gardener | triages 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.