repolane v0.11.0 · open source

Guardrails

Extending

Your own refusals in .repolane/registry/rules.yaml, your own lane commands, shared team rules with lane rules pull, per-repo secret files, and sharing a workspace's files with a team.

On this page

In brief. Four ways to extend Repolane without forking it, all inside your workspace: project rules in .repolane/registry/rules.yaml, per-repo secret names via lane secrets, your own scripts in .repolane/scripts/local/, and shared rules via lane rules pull. The first two can only add refusals. The last two run code or feed text to sessions, so trust the source as you would your own scripts.

There are four ways to add to Repolane without forking it, and each one has a different trust story. All four live in your workspace, never in the installed package, so an upgrade never touches them. They are in order of how much they can do to you, least first.

You wantMechanismCan it run code or change what a session reads?
a refusal specific to your project.repolane/registry/rules.yamlno. It can only add a deny
a per-repo secret filename deniedlane secrets <repo> addno. It writes deny rules into the file above
your own lane <name> commandan executable in .repolane/scripts/local/yes: it is a script you run
the same rules, scripts and notes across a teamlane rules pull <git-url>yes: it copies scripts and notes in

Rules of your own

The rules that ship with Repolane are true of every project. Ones that are true only of yours (a migration the entrypoint runs, a branch that deploys itself) go in .repolane/registry/rules.yaml, in your workspace. It never reaches the package or anyone else unless you commit it to a repo you share. A rule can only add a refusal: the loader has no syntax for granting anything, and a project rule is checked before the built-ins, because a few of those answer allow and stop.

rules:
  - id: no-manual-migrations
    when: { tool: Bash, matches: '\balembic\s+(upgrade|downgrade)\b' }
    deny: "entrypoint.sh applies migrations on every start. Restart the service instead."
    ref: memory/api/migrations.md
    example: "alembic upgrade head"

  - id: no-editing-generated-schema
    when: { tool: Edit, path: 'schema/generated/**' }
    deny: "schema/generated/ is written by codegen. Edit the source schema and regenerate."
    ref: docs/codegen.md
    example: { path: "schema/generated/types.ts" }
Field
idhow the refusal names itself, and what lane doctor reports. Must be unique across every rules file
when.toolthe tool name as Claude Code spells it: Bash, Edit, Write, Read, … Only tools in the hook matcher are ever consulted (see Security model); Grep and Glob are in it, but WebFetch and MCP tools are not, so a rule for those never fires
when.matchesa Python regular expression, for Bash. One rule has matches or path, never both
when.patha glob, for the file tools. Matched against the path as written and every tail of it
denywhat Claude is told. Say what to do instead, not just “no”
refrequired. The file that says why. A refusal nobody can check is one that gets routed around
examplea case the rule must refuse. lane doctor feeds it through the guard on every run

Values are single-line: the parser reads a small YAML subset and reports anything else as an error with a line number rather than half-understanding it. matches is tested against the same normalised command segments as the built-ins, so echo ok && alembic upgrade head is judged like alembic upgrade head.

When a rules file is wrong (no ref, an invalid regex, a duplicate id), those rules are not enforced, the hook says so on stderr, the built-in rules carry on, and lane doctor fails and names each one. lane doctor verifies that each rule refuses its example, and that it is refused by that rule. It does not notice if the whole file is deleted. A session inside a lane cannot write or delete it (.repolane/ is protected from a lane), but you can, from the root or your terminal (self-protection). Keep the workspace in a git repo of your own if it matters.

Per-repo secret files

The built-in secret rule recognises a fixed set of names; see Security model. Names it cannot know about (service-account.json, .npmrc, id_rsa, .aws/credentials) are declared per repo.

lane add scans a new repo for filename patterns that look like credentials, by name only and never by reading contents. Most candidates turn out to be ordinary source (password.py, token_store.py), so it asks before denying anything, and with no terminal it only lists them.

Command
lane secrets <repo> scancandidate names, one per line, excluding ones you dismissed
lane secrets <repo> add <path-or-glob>…writes deny rules (one each for Read, Edit, Write, MultiEdit, NotebookEdit and Bash) that make a file with that name unreadable and unwritable to every future session. The rule matches the name wherever it appears, in any repo and lane, not only in <repo>
lane secrets <repo> ignore <path-or-glob>…mark a candidate reviewed-and-not-a-secret, in .repolane/registry/secrets-ignored.yaml; no deny rule
lane secrets <repo> list, … ignoredwhat is denied, what is dismissed

These rules are in .repolane/registry/rules.yaml, so they carry the limits of any project rule: they are checked for the tools in the matcher, a shell command that builds the filename by glob or quoting (cat web/service-acc*) is not recognised, and a session in a lane cannot edit or delete the file (.repolane/ is protected from a lane session). The Bash rule is a regex over the whole command, so it also refuses harmless mentions (ls web/service-account.json). lane secrets <repo> add and ignore are refused from inside a lane; run them from the workspace root or your terminal.

Your own lane <name> commands

Any executable file in .repolane/scripts/local/ becomes lane <name>:

mkdir -p .repolane/scripts/local      # it does not exist in a new workspace
cat > .repolane/scripts/local/hello <<'EOF'
#!/usr/bin/env bash
# lane: say hello
echo "hello from .repolane/scripts/local/hello"
EOF
chmod +x .repolane/scripts/local/hello
lane hello
  • .repolane/scripts/local/ is part of your workspace, never of the package: an upgrade never touches it. With lane init --git it is tracked like your other files.
  • A second line of the form # lane: <one-liner> is what lane help shows next to it.
  • A local script is a full peer of a built-in: it runs with LANE_PRODUCT (the installed package’s files) and LANE_WORKSPACE (the workspace) set, and can source "$LANE_PRODUCT/scripts/lib.sh" to use the same helpers.
  • Built-ins win. A local file with the name of a real command never runs; lane doctor flags it as shadowed.
  • The command name is a bare filename. A name containing / is rejected before any lookup.

A local script is code that runs with your account’s access, and the guard does not gate lane <name> any differently from other workspace commands.

Shared rules: lane rules pull

A team can keep one set of rules, scripts and notes in a git repo and pull it onto each machine:

lane rules pull <git-url-of-your-rules-repo>                 # first pull; records the commit SHA
lane rules pull                                              # re-fetch; refuses to move past the pinned SHA
lane rules pull --latest                                     # intentionally move the pin
lane rules status                                            # URL, pinned SHA, remote HEAD, what is loaded

The shared repo holds rules.yaml (required, the same schema as above), and optionally scripts.d/ and knowledge.d/.

How it is made safe, on purpose:

  • Pinned, never tracking a branch tip. The first pull records the exact commit SHA in .repolane/config.yml. Only --latest moves it.
  • Copied, never symlinked. The clone lives in .repolane/registry/.shared-cache/, which is machine-local (lane init --git ignores it). Nothing points into it. A file in scripts.d/ does not overwrite a script of the same name that you put in your .repolane/scripts/local/: it keeps yours and says so (unless an earlier pull put that file there, tracked in .repolane/cache/scripts-pulled, in which case it is updated).
  • A separate file. Rules land in .repolane/registry/rules.shared.yaml, never merged into your own. Ids are namespaced so they cannot shadow one you wrote. Duplicate ids are rejected, loudly.
  • Rules can only deny. Shared rules go through the identical parse, validate and deny-only path as your own.
  • No remote Python. It does not pull custom guard logic from a remote.

What comes with a shared rules repo

Read this before you point lane rules pull at a repo you do not control. The rules are the safe part. The other two parts are not rules:

  • scripts.d/ becomes executable commands. Files are copied into .repolane/scripts/local/ and become lane <name>. You can read them before they land, but once they do, running one is running that repo’s code as you.
  • knowledge.d/ is injected into every session. Markdown is copied into .repolane/knowledge/shared/ (never into .repolane/knowledge/ itself, so it cannot overwrite a note of yours), and every .md file there is added to each lane session’s first-prompt context, the same way .repolane/knowledge/preferences.md is. A hostile or careless note there is an instruction the model reads, in every session, in every lane. The guard does not and cannot filter it. Treat the shared repo as having the same authority over your sessions as your own CLAUDE.md.

lane doctor prints an advisory when the pinned SHA is behind the shared repo’s HEAD. To do so it runs git ls-remote against that repo, which is a network call.

Sharing a workspace’s files with a team

Your workspace can be a git repo of your own (Install), and nothing in it is ignored except what is machine-local. So .repolane/registry/repos.yaml, .repolane/registry/rules.yaml and .repolane/scripts/local/ are tracked like any other file you commit: a team that wants everyone to start from the same repo list, rules and commands can keep one workspace repo and clone it on each machine. What should stay per person (.repolane/memory/, .repolane/knowledge/, the lane files) is up to you: leave it out of the shared repo, or add it to .gitignore.

Those files can hold local absolute paths (repos.yaml records old_paths: for adopted checkouts), so push them only to a private remote. For rules alone, lane rules pull (above) is the lighter way: every machine pins the same commit of a separate rules repo, and nobody’s own files are touched.

Next steps