repolane v0.11.0 · open source

Guardrails

Rules

Every refusal Repolane makes, the failure behind each one, and which rules a hook enforces and which are instructions.

On this page

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.

RuleHow it is held
Stay in your lane: other lanes, the base clones, the old checkoutshook (refusals), plus deny rules in the lane’s own settings
Nothing under your home directory outside the workspacehook, 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 lanehook, 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-onlyhook for writes; shell reads pass
The workspace is edited from the roothook, from inside a lane (see Security)
Never read .env or credential fileshook, 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 remoteshook, 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/ directlyhook
Memory goes through lane note; you pick the scopeinstruction. lane-memory file is not pre-approved (it prompts), but nothing makes a session ask you which scope
New branches start from the default branchlane start does this by default. Nothing stops a session running git checkout -b from somewhere else
Ask before committinginstruction. git commit in a lane returns no decision from the guard
Discuss before deciding; one question at a timeinstruction

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 shortHeld by
1Discuss before deciding; run directly only what is mechanical, reversible and asked for; one question at a timeinstruction
2Stay 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 pathhook for cd; instruction for the rest
3From the dispatcher root a lane’s files are out of reach; enter it or use lane-brief / lane-statushook
4Work only inside the current lane folder; do not read other lanes or .repolane/repos/ unless the lane file lists ithook (with the gaps in Security)
5Read 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 workinstruction
6Nothing outside the workspace is yours to read; attach it with lane-ref and read the reference pathhook, for paths under your home
7Never change remotes or touch main/master/develop; a push to a default branch needs the owner’s word and a visible markerhook, see Git and GitHub; the marker is advisory
8Never read .env or credential files; use .env.example or lane-env-keys <path>hook, by file name
9New branches start from the repo’s default branchlane start; otherwise instruction
10When parking or finishing, update the lane file: it is the handoffinstruction
11Shared build caches and environment settings go in .repolane/env.shinstruction
12Never 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 goalhook blocks direct writes; the question is an instruction
13One checkout exists for you: the worktree you are in; report git state in the STATE forminstruction (the hook blocks the frozen checkouts)
14Inside a lane, never create another worktree or lane; use lane-with <id> <repo>hook for lane-start, lane start and git worktree add
15Talk plainly; prefer an ASCII diagram to a long explanation; never create an artifact without askinginstruction

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.

A real session in 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.

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

From a terminal, 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.

Next steps