Guardrails
Rules
Every refusal Repolane makes, the failure behind each one, and which rules a hook enforces and which are instructions.
On this page
- Enforced by the hook, and instructed in context
- The numbered rules a session is given
- Stay in the lane you are in
- Do not enter a repo
- The base clones are read-only
- The workspace is edited from the root
- Never read .env or credential files
- Pushing to a default branch needs your word
- Memory is never written directly
- New branches start from the default branch
- Ask before committing
- What lane park commits
- Rules of your own
In brief. This page lists every refusal Repolane makes and why, and says which rules a hook enforces and which are only instructions to the model. Remember: a hook can check a path or a command, but it cannot check that Claude asked you first. Anything marked instruction is a request, not a guarantee.
Every refusal here exists because the alternative went wrong. If you hit one, this page says why.
These refusals come from a hook that reads each tool call before it runs, which is a different mechanism from an instruction in a prompt. The hook is a guardrail, not a sandbox, and it judges only the tool calls it is shown. How it fails, and what it never sees, is set out once in the security model; where a rule below has a known limit, it says so.
Enforced by the hook, and instructed in context
Some rules on this page are instructions Repolane puts in front of the model (.repolane/REPOLANE.md,
which your CLAUDE.md imports; .repolane/LANE.md inside a lane; each prompt’s injected context). A hook can verify a path or a command; it cannot verify
that Claude asked you a question first.
| Rule | How it is held |
|---|---|
| Stay in your lane: other lanes, the base clones, the old checkouts | hook (refusals), plus deny rules in the lane’s own settings |
| Nothing under your home directory outside the workspace | hook, for paths under ~/, your home directory itself (~, $HOME) and another account’s home, after symlinks are followed. Paths outside $HOME (/etc) and /tmp are not judged |
Do not enter a repo; do not cd out of the lane | hook, for every spelling of cd (pushd, builtin cd, \cd, { cd x; }, eval cd x), with the directory simulated statement by statement |
| The base clones are read-only | hook for writes; shell reads pass |
| The workspace is edited from the root | hook, from inside a lane (see Security) |
Never read .env or credential files | hook, by file name, with one list in the file tools and the shell; and Claude Code’s own permissions.deny for Read and Edit in the lane’s settings |
| No pushing to default branches; no changing remotes | hook, which reads the push’s refspecs, so main;, +main, 'main', --mirror, --all and a bare git push on a protected branch are refused. The ALLOW_DEFAULT_BRANCH_PUSH=1 marker is advisory |
Memory is never written to .repolane/memory/ or .repolane/knowledge/ directly | hook |
Memory goes through lane note; you pick the scope | instruction. lane-memory file is not pre-approved (it prompts), but nothing makes a session ask you which scope |
| New branches start from the default branch | lane start does this by default. Nothing stops a session running git checkout -b from somewhere else |
| Ask before committing | instruction. git commit in a lane returns no decision from the guard |
| Discuss before deciding; one question at a time | instruction |
The numbered rules a session is given
Every session in a workspace reads .repolane/REPOLANE.md, a numbered list of rules, through the line
@.repolane/REPOLANE.md in your CLAUDE.md. Repolane writes that file and writes it again after an
upgrade; it does not carry your name (the hooks name you when a session starts), and your own notes
belong in CLAUDE.md, never in it. Until the import line is there the rules do not load; lane doctor
checks it. This is what the recordings mean when the model says “rule 8”. Short forms, with how each is
held (the file is the authority):
| # | The rule, in short | Held by |
|---|---|---|
| 1 | Discuss before deciding; run directly only what is mechanical, reversible and asked for; one question at a time | instruction |
| 2 | Stay in your lane; never cd out of it (the way back is EnterWorktree); the lane commands run from anywhere without a cd; reach another repo of the lane with git -C or an absolute path | hook for cd; instruction for the rest |
| 3 | From the dispatcher root a lane’s files are out of reach; enter it or use lane-brief / lane-status | hook |
| 4 | Work only inside the current lane folder; do not read other lanes or .repolane/repos/ unless the lane file lists it | hook (with the gaps in Security) |
| 5 | Read the lane file before coding; memory indexes arrive with the first prompt and a repo’s own CLAUDE.md the first time it is touched; open what matches the work | instruction |
| 6 | Nothing outside the workspace is yours to read; attach it with lane-ref and read the reference path | hook, for paths under your home |
| 7 | Never change remotes or touch main/master/develop; a push to a default branch needs the owner’s word and a visible marker | hook, see Git and GitHub; the marker is advisory |
| 8 | Never read .env or credential files; use .env.example or lane-env-keys <path> | hook, by file name |
| 9 | New branches start from the repo’s default branch | lane start; otherwise instruction |
| 10 | When parking or finishing, update the lane file: it is the handoff | instruction |
| 11 | Shared build caches and environment settings go in .repolane/env.sh | instruction |
| 12 | Never write memory or preferences directly; four scopes; draft, ask the owner, then file. Change the workspace’s own files from the root. In a lane, set the Goal with lane-memory goal | hook blocks direct writes; the question is an instruction |
| 13 | One checkout exists for you: the worktree you are in; report git state in the STATE form | instruction (the hook blocks the frozen checkouts) |
| 14 | Inside a lane, never create another worktree or lane; use lane-with <id> <repo> | hook for lane-start, lane start and git worktree add |
| 15 | Talk plainly; prefer an ASCII diagram to a long explanation; never create an artifact without asking | instruction |
A session that enters a lane, or starts inside one, also gets .repolane/LANE.md: six short rules for
working inside a lane, and the commands to use because Claude Code refuses Edit and Write outside the
lane folder (lane-memory goal for the Goal, lane-memory draft for a memory, lane-park for a resume
note). See Working with Claude.
An instruction is a request to the model that sits in its context. A session may follow it without the hook ever firing, or not follow it; nothing verifies it.
Stay in the lane you are in
A session belongs to one piece of work. Reads and writes aimed at another lane, at the base clones
(a shell read of a base clone is the exception), or at your home directory outside the workspace are
refused by the hook, and for Read and Edit independently by deny rules in the lane’s own settings.
Without the rule, a session asked to fix one bug can read unrelated repos and fill its context with code
nobody asked about.
/tmp and everything outside your home directory is allowed; $HOME itself is judged and
expanded. The exact scope is in the security model. The rule is
about a session’s focus and about your own files under ~; it is not “nothing outside this folder is
readable”.
Need something outside? Attach it once with lane ref and read it at lanes/<id>/refs/<name>.
Reaching outside is sometimes legitimate; what the rule removes is doing it silently. References are
read-only unless you attach them with --rw. The secret-file rule is applied first, so a .env inside
an attached folder is refused like any other, and lane-ref add refuses /, your home directory,
system and credential directories and secret files. A credential under a name the guard does not know
is an ordinary file in a reference. From inside a lane, lane-done, lane-park, lane-resume and
lane-with act only on the lane you are in, and lane-ref <other lane> add|rm is refused.
- 2026-09-18 13:13 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)
❯ check what the SHOP-412 lane changed in web and summarise it
⏺ That one's outside this lane, so I can't look at it from here.
- I'm in lane SHOP-418, whose repos are api and mobile. web isn't in it, and SHOP-412's files are
another lane's.
- A lane doesn't reach out — no reading lanes/SHOP-412/…, and the hook would refuse it anyway.
Two ways to get it:
1. Fastest: park this lane, leave to the root, and run lane brief SHOP-412 there — that reads the
lane file for you and I can summarise from it.
2. If SHOP-412's web work is genuinely needed for SHOP-418, attach it as a reference: lane ref
SHOP-418 add <path-to-web-worktree> "SHOP-412 web changes for comparison", then I read it at
refs/… without leaving.
I'd go with (1) if you just want the summary, (2) if mobile needs to match what web did. Which
one?
✻ Churned for 4s · done 1:13 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.
SHOP-418, asked about a different lane. It refuses, then offers the two ways round it: park this lane and ask from the root, or attach the other worktree with lane ref and read it at refs/ without leaving. 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.Adopted checkouts are frozen. When you lane add a checkout you already had, its path is recorded
and the guard refuses a session any read, edit or shell mention of it, so the stale copy is never
edited by mistake.
Do not enter a repo
The session sits at lanes/<id>/, never inside lanes/<id>/<repo>/. Entering one makes that repo look
like the main one, and multi-repo work degrades into single-repo work. A cd moves the session, so
cd out of the lane, or into a repo, is refused. Nothing needs it: lane run <repo> <cmd>,
lane gh <repo> <args>, git -C <repo>, and editing <repo>/file all work from where you are.
The base clones are read-only
.repolane/repos/<name> stays on its default branch and clean, because every worktree shares its object
store. Work in a worktree. Writing to a base clone, and checkout, reset, commit, merge,
rebase and friends inside one, are refused. Reading one from the shell is fine; a file-tool
Read of a base clone from inside a lane is refused.
The workspace is edited from the root
Your own commands, rules, the registry, memory, CLAUDE.md and .claude/settings.json are changed from
a session started at the workspace root, not from inside a lane, including through the shell by
absolute path, so echo x > …/.repolane/registry/rules.yaml is refused exactly where Write is. The
installed package’s own files are refused the same way. A lane editing the rules that constrain it is
how you lose the rules.
At the root there is no lane, so this rule does not apply there. The root is where you maintain the
system, and lane doctor exists to tell you the guard is still the guard.
The protected set is everything under .repolane/ (the lane’s own spec excepted, and in the cache the
memory inbox and the nx build cache), so registry/, rules.yaml, rules.shared.yaml, config.yml,
env.sh, scripts/local/ and the rule files; every file at the workspace root, CLAUDE.md included;
everything under .claude/, a lane’s own settings.local.json included, by file tool or by relative
path; the installed package; and git -C <workspace> checkout|reset|clean|apply. Memory and knowledge
get the memory procedure instead (below). Workspace commands (lane upgrade, init, migrate, add,
rules pull, secrets add|ignore) are refused from inside a lane. Reads of the workspace
pass. The security model has the detail.
Never read .env or credential files
Not even when you grant permission: the read itself is the problem, because the values end up in a
transcript. Copying first does not help: cp .env /tmp/x && cat /tmp/x is refused at the cp, because
once a command names a recognised secret file, every part of it that does so has to start with one of a
few verbs that reveal nothing and cannot move the file.
The rule matches by file name, with one list for the file tools and the shell: .env and .env.*;
id_rsa, id_ed25519, id_ecdsa, id_dsa; .netrc, .npmrc, .pgpass, .git-credentials,
.htpasswd; *.pem, *.key, *.p12, *.pfx, *.p8, *.jks, *.keystore, *.kdbx;
*credentials*.json, service-account*.json; secrets.json|yaml|yml|toml. Case-insensitive; a
.example, .sample, .template or .dist suffix is exempt, unless that file is a symlink to a
secret. Names are read after the shell would have read them (variables set earlier in the command, brace
expansion, $'\x2eenv', $(echo .env)), and ls $(cat .env), file -f .env and find -exec cat are
refused. A whole folder is a read of everything in it: grep -r, tar, cp -r, rsync and the Grep
tool are refused over a folder that holds a secret unless they leave it out. Use
grep -r --exclude='.env*', rg -g '!.env*', or a Grep glob of "!.env*"; the denial says which. A
file full of credentials under a name not on the list (.envrc, *.tfvars, .aws/credentials) is an
ordinary file to the guard unless you add a per-repo rule. Interpreter one-liners are searched for names
and the common ways of assembling one, not parsed: a name computed from arbitrary data is not seen. The
exact lists and what the guard does not see are in the security model.
In a session, reading a .env has two layers: the guard, and Claude Code’s own permissions.deny in the
lane’s settings. For a Read of a lane’s .env the second answered in live runs (observed on Claude Code
2.1.299, headless); a Bash cat of the same file was refused by the guard.
Structure is available instead, via .env.example or lane-env-keys, which lists key names and never
values. The installed lane-env-keys and lane-env-check stubs map to lane keys and lane env. From
your own terminal lane keys <path> works too.
A symlinked .env in a lane. lane start links a repo’s env files into the lane from the read-only
mirror, so writing the link would change the mirror. The refusal says so. For a local file: rm <the link> (this removes only the link), cp .env.example .env, then ask the owner for values. Writing a
new env file (echo X=1 > api/.env.local) gets a denial worded for a write, not a read.
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, and offers lane-env-keys and .env.example. The hook is the backstop if it had tried 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.lane-env-keys inside a session, lane keys in your terminal. Inside a Claude session
lane keys web/.env is itself refused, because the command line names a secret file and lane is
not on the allow-list; only the bare word lane-env-keys passes the guard. From your own terminal
lane keys web/.env works.
~/work $ lane keys lanes/SHOP-418/api/.env
variables in lanes/SHOP-418/api/.env (names only):
DATABASE_URL
PROMO_SIGNING_KEY
SESSION_TTL_MINUTES
STRIPE_SECRET_KEY
~/work $ exit
The last screen of a real asciinema recording, as text. With
JavaScript the player replays the whole session from the start.
lane keys lists every key name in the file and not one value. (Inside a Claude session the guard refuses that spelling; see the note above about lane-env-keys.)Pushing to a default branch needs your word
main, master and develop are refused when a push command names them, plus a repo’s own
default_branch/compare_branch (.repolane/registry/repos.yaml, set by lane add). Once a repo has been
checked with lane sync, its real GitHub branch protection is added too. When you do mean it, the
command starts with a marker you can see: ALLOW_DEFAULT_BRANCH_PUSH=1 git push ….
The marker is a visible flag, not an authorisation. It is advisory. Claude can type it as easily as
you can, so it records that the human agreed; it does not prove it. It works per statement (it does not
excuse a second git push later in the same line) and it never overrides a force-push. A hard version
needs something the model cannot produce, for example you exporting the variable in the environment
Claude Code is launched from, which the hook inherits and the command text does not show. That is not
implemented.
The guard reads the push, not a pattern: git push origin main;, main&&…, +main, 'main',
HEAD:refs/heads/main, :main, --delete main, --all, --mirror and a bare git push while a
protected branch is checked out are all refused, including inside bash -c, eval, a heredoc, xargs or
an env -S string. A push of your own lane branch, with or without -u, passes, and so does a force-push
of an unprotected branch. git remote add|set-url|remove|rename, git config remote.<name>.url,
url.*.insteadOf and anything that defines a git alias or changes where hooks come from
(core.hooksPath, include.path, GIT_CONFIG_*) are refused. See the
table in the security model.
Memory is never written directly
A session that tries to write under .repolane/memory/ or .repolane/knowledge/, by file tool or by
shell, is refused with the procedure to follow: lane-memory draft <slug> "<text>" (or - and the
text on stdin), ask which of the four scopes it belongs in, then lane-memory file <slug> <scope>.
lane-memory is lane note under its long name; a session uses the long name because Claude Code
refuses Edit and Write outside the lane folder, so the draft has to go in through the command. That last part is an instruction. The guard
blocks the direct write. lane-memory list|show|audit|draft|goal are pre-approved, in a lane and at the
workspace root, so they run without a prompt, and file and drop prompt; but nothing forces the question to you, and a session that is allowed to run lane-memory file can
choose the scope itself. Slugs and names are validated: lane-memory drop ../../x is refused (see
the security model). Without the question, a passing detail can be
filed as a permanent global preference and stay after it stops being true. The scope is yours to decide,
and you hold that line by reading what is filed.
New branches start from the default branch
lane start branches from the repo’s default branch unless you say otherwise
(lane start ABC-123 web@release-2, or --base <ref>). If a branch of that name already exists it is
checked out as it is. A session can still run git checkout -b from a different starting point; the
workspace’s rules tell it not to.
Ask before committing
Commits are proposed and shaped as coherent units: neither one commit per edit nor one enormous
commit at the end. This is an instruction (your preferences in .repolane/knowledge/preferences.md
say so by default). There is no hook on
git commit inside a lane.
What lane park commits
lane park is the command that stops work for now. The guard does not look inside it (it is one of the
lane-* scripts the guard trusts), so it carries a secret-name filter of its own. It wip-commits every
dirty worktree of the lane: modified and deleted tracked files, and untracked files that git does not
ignore. It never commits a file whose name says secret (it stages by explicit literal path, so a file
called .env* cannot pull .env in through a glob). Those files stay on disk, untracked, and it prints
left out of the commit (secret file names, still on disk): …. The names are the guard’s list from Never read .env or credential files; templates ending .example, .sample, .template or .dist are not secrets. A secret that is already staged
stops the park: nothing is committed in that worktree (one earlier in the loop may already have its wip
commit) and the lane is not marked parked. Run git -C <worktree> reset -- <file>, then park again. A
rename or copy is checked on both sides.
It does commit a file with secret-looking content under an ordinary name, or a name not on that list
(.envrc, *.tfvars). Look at git status before you park, and ignore or name what should not go in.
lane done then deals with the left-out files: it names them and tells you to move them or pass
--discard-ignored (see Troubleshooting).
Rules of your own
The rules above ship with Repolane because they are true of every project. Ones that are true only
of yours go in .repolane/registry/rules.yaml; see Extending. A rules file can only add a
refusal, never grant anything.
lane doctor checks that the guard is healthy and in force: it compiles the guard, fires its named
probes at it (including one that must not be refused, because a guard that denies everything is
also broken), checks that each of your own rules still refuses its example, and walks every lane and
worktree to confirm each still points at the guard. A lane whose settings lost their hooks is
unguarded even when the guard itself is healthy; lane resume <id> repairs the wiring.
For what the hook can and cannot see in the first place, read the security model.