Using lane

A lane is a throwaway worktree that costs almost nothing to open, and leaves something behind when it closes.

$ lane new fix-login        # branch + worktree, the build cache by reference
# work, commit as usual
$ lane merge                # rebase, audit memory, fast-forward, delete

Setup

$ curl -fsSL https://lane.lukeed.com | sh
$ eval "$(lane shellenv)"   # add to .zshrc: makes `lane new` and `lane enter` cd
$ cd yourproject && lane init
$ git add .lane AGENTS.md
$ git commit -m "lane: context memory"

lane init prints whether your filesystem supports reflink:

reflink on this filesystem: yes (reflink available)

yes on APFS, btrfs, XFS with reflink=1, bcachefs, recent ZFS. no means lanes still work as plain worktrees. Ignored files are not cloned because a full byte copy would do the expensive work lane exists to avoid.


Daily flow

Open a lane

$ lane new fix-login
  reflink: yes (reflink available)
  1284 files cloned (612.4 MiB shared, 0 copied)
  /Users/you/yourproject/.lane/trees/fix-login

Tracked files come from git. Everything git ignores, at any depth, arrives by reference — nested node_modules, target, .env, and whatever your own ignore rules name. Uncommitted work is not carried. The shared bytes mean no copy, no reinstall, and no cold rebuild.

Carry uncommitted work across too:

$ lane new spike --dirty
  carried 3 uncommitted change(s) from the parent tree

--dirty does the same as the default and also carries your uncommitted work. Use it when you want to hand your current mess to an agent and keep working yourself. Without reflink support, either mode leaves a plain worktree.

Opt a gitignored entry out with multi-valued configuration:

$ git config --add lane.exclude target
$ git config --add lane.exclude packages/legacy/node_modules

Leave notes while you work

There are two ways in. Both write a note directly to .lane/memory/, where it is readable and visible to Git immediately. The next audit records its first baseline after any rebase. Use whichever suits the moment, or both.

Option one — a command, whenever you notice something.

$ lane note add src/auth.rs -a "fn verify" \
>   "must stay constant-time; early return leaks token length"

Supplied text never prompts and an omitted -a defaults to @file. Omit the text only when you want an interactive anchor selector and one-line note prompt.

Option two — a trailer, in the commit you were already writing. Install the hooks once per repository, then never think about it again.

$ lane install hooks

$ git commit -m "make verify constant-time
> 
> Why: src/auth.rs#fn verify | early return leaks token length"

Option one costs a separate decision at the moment you are thinking about the commit, which is the one you forget. Option two costs a line. Neither is more correct than the other.

The target path is required; omit #<anchor> to use @file. Record why it must stay true, not what you did.

-a is the anchor — what the note is about:

anchor matches
fn verify a declaration by keyword + name
verify any declaration of that name
#script, #style top-level blocks in .svelte/.vue/html
## Rate limiting a markdown section
@file the whole file (default)

Discover the exact canonical values and their inclusive line ranges before recording a note:

$ lane anchors src/auth.rs
@file       1-8
fn verify   1-4
fn refresh  6-8

Use lane anchors <path> --json for structured output. A unique bare name such as verify is stored as its canonical value; a name shared by multiple declaration kinds is refused and lists the available choices.

One note, one thought. Don’t classify it, don’t decide whether it’s an invariant or a rationale — the whole point is that a note costs a single command with no taxonomy decision.

Read what earlier lanes learned

$ lane why src/auth.rs

[fn verify]
  - 01M0B4KQTX · 2026-07-30
    callers rely on false-on-expiry, not an error
  - 01M0B9MBYB · 2026-08-14
    must stay constant-time; early return leaks token length

The path is omitted from headers because the command already names it. A directory reads the whole subtree beneath it, and a bare lane why reads the whole store; both keep path#anchor in each header so groups remain unambiguous:

$ lane why src/

[src/auth.rs#fn verify]
  - 01M0B9MBYB · 2026-08-14
    must stay constant-time; early return leaks token length

[src/db/pool.rs#@file]
  - 01M0C1PDQR · 2026-08-15
    the pool is created once per process; a second one exhausts the server

Matching is by whole path component, so src never claims src-gen/lib.rs. lane why changes nothing and carries no branch provenance. Run lane check to see whether a note’s anchored code has drifted and get the id needed to resolve it.

Close the lane

$ lane merge
  rebased onto main
  memory: +0 new; checked 8: 7 fresh, 1 content-changed, 0 contract-changed, 0 missing
  committed memory update
  fast-forwarded main
  removed lane fix-login

merge never touches the network for git. Add --keep to preserve the worktree, --squash to land one commit, or --base <name> to use a different base. Use lane push for a pull request — see Pull requests.


Pull requests

Where trunk is protected, lane push rebases, audits, commits memory, and pushes the lane:

$ lane push

Turn on Require branches to be up to date before merging. The audit fingerprints spans against the post-rebase tree, so a pull request merged on a stale base describes a tree nobody has. That setting serializes two clones the way the landing lock serializes two lanes on one machine.

The lane stays on disk until the pull request merges. lane ls marks it pushed while the remote has its tip, then landed once trunk carries its landing record; lane prune removes it. The marker is tree content rather than a commit, so neither a squash nor a rebase merge can hide it — git branch -d refuses both even when the trees are identical. It names the lane rather than the branch, because fix twice in a week is normal and the second one has landed nothing. Prune still checks that nothing on the branch is missing from trunk, so work committed after the merge is never discarded, and it removes nothing you are standing in.

What happens to notes over time

Every audit re-resolves each anchor and hashes only that anchor’s span, normalized so comments and whitespace don’t count:

tier meaning what happens
fresh unchanged nothing, costs nothing
content-changed implementation moved flagged until you resolve it
contract-changed the described thing changed shape flagged until you resolve it
anchor-missing symbol gone evicted to .lane/attic/
unverifiable no grammar for this anchor reported for manual review

A renamed or moved file is followed, not evicted: lane audit reads git’s own rename detection and moves the notes with it. Eviction means the file or the symbol is genuinely gone.

A drifted note stays flagged until you run lane note confirm <id>, replace it with lane note replace <id>, or run lane note retire <id>. Until then, lane check keeps reporting it.

Editing #script never stales a note on #style. Running a formatter stales nothing at all.

The two drift tiers split a span at its declaration line: fn verify(t: &str), <script>, ## Rate limiting. Only an anchor that has one can report contract-changed. An @file note has no declaration — its first line is an import or a shebang — so every change to it is content-changed. A heading’s declaration is its anchor, so changing it reports anchor-missing, not contract-changed.

Resolving drift

Before lane merge, run lane check. It lists every note that is not fresh with the id you need next:

$ lane check
fresh              7
content-changed         1
contract-changed  0
anchor-missing     0
unverifiable       0

[content-changed]
~ 01M0B4KQTX  src/auth.rs#fn verify

Read the note and the code it points at, then take one action: lane note confirm <id> when the sentence remains true; lane note replace <id> "<rewrite>" when the subject is right but the sentence must change; or lane note retire <id> when the constraint is gone. Replacement inherits the predecessor’s path and anchor unless you override them. At a terminal, lane note edit <id> presents these actions plus pin/unpin as a guided menu. Lane never calls a model.

Any unambiguous prefix of an id works, so the ten characters above are enough; an ambiguous one is refused and names what it matched. Add --json for the same rows plus each note’s body and current span, which is what an agent reads.

Replace writes a new file and moves the predecessor to the attic immediately. A ? has no grammar and cannot be resolved. An x means the symbol is gone, so audit moves the note to the attic instead of vouching for it.

Budget

Each (file, anchor) holds at most 5 notes / 1200 characters. Audit ranks by pinned > touched by this lane > freshness > age and moves the rest to .lane/attic/; its location is the retirement record. Nothing is deleted.

Keep something permanently:

$ lane note pin 01M0B4KQTX

Use lane note unpin <id> to return it to ordinary retention. Recover something:

$ lane note restore 01M0B4KQTX

Retire and restore move the note bytes unchanged.


Working with agents

lane init writes the protocol into AGENTS.md. Symlink CLAUDE.md to it if you keep both:

## Context memory
- Before editing a file, read `.lane/memory/<path>/` if it exists, or run `lane why <path>`.
- Record non-obvious findings with `lane note add <path> -a <anchor> "..."`.
- Do not edit `.lane/` by hand; landing manages it.
- Land with `lane merge`, or `lane push` where trunk is protected, then `lane prune` once it merges.
- Detailed workflow lives in `.agents/skills/lane/SKILL.md`; run `lane install skill` if it is absent.

That stub is always in context and stays short. lane install skill writes the fuller version — the daily loop, the Why: trailer form, the anchor grammar — to .agents/skills/lane/SKILL.md, loaded only when an agent is doing lane work.

Notes are plain markdown at predictable paths, so an agent finds them without any tool integration — the reason to store them as files rather than in a sidecar object store.

Run several agents at once:

$ lane new agent-a && lane new agent-b && lane new agent-c
$ lane ls
  agent-a    open     clean   3 pending note(s)
  agent-b    landed   dirty   1 pending note(s)

For a stable machine-readable inventory, orchestrators can use:

$ lane ls --json
[{"name":"agent-a","path":"/w/proj/.lane/trees/agent-a","branch":"agent-a","state":"open","dirty":false,"pending_notes":3}]

Before editing a file, lane why <path> --json provides its full note ids, timestamps, anchors, and text without scraping the compact human view. If the anchor is not already known, lane anchors <path> --json supplies the canonical candidates without requiring the agent to guess.

Each has its own warm build cache at no disk cost. They can annotate the same file, the same anchor, at the same time: each new finding gets its own note file, so there is nothing to lock and nothing to conflict.

Land them in any order.


Reference

The short version. See commands for full information.

command
lane init scaffold, probe reflink
lane new <name> [--dirty] [--base <ref>] create a lane
lane ls lanes, whether they landed, dirt, pending notes
lane enter <name> change directory into a lane. Or switch alias
lane exit change directory back to the main worktree
lane anchors <file> [--json] list canonical anchors and line ranges
lane note add <file> [-a <anchor>] [<text>] record a finding; omit text for interaction
lane note edit <id> interactively confirm, replace, retire, pin, or unpin a live note
lane note replace <id> [<text>] replace a live note, inheriting path and anchor
lane note confirm <id> re-vouch for a drifted live note
lane note retire <id> move a live note to the attic
lane note restore <id> restore a retired note
lane note pin <id> protect a live note from eviction
lane note unpin <id> remove eviction protection
`lane install skill hooks`
`lane uninstall skill hooks`
lane why <path> [-a <anchor>] read the notes on a file or a directory; changes nothing
lane check [--json] staleness report; exits 1 on missing anchors
lane audit [--base <ref>] run the memory pass alone
lane merge [<name>] [--keep] [--base <ref>] [--squash] rebase, audit, fast-forward, remove
lane push [<name>] [--base <ref>] rebase, audit, commit memory, and push for a pull request
lane prune [--dry-run] remove lanes whose branch has landed in trunk
lane rm <name> [--force] discard a lane; it stops and names uncommitted work, pending notes, or commits trunk does not have, --force drops them
lane shellenv shell integration

Layout

yourproject/
  .lane/
    memory/src/auth.rs/01M0B9MBYB-must-stay-constant-time.md   the note and its baseline
    attic/                        evicted, recoverable
    trees/
      fix-login/                  the lane worktree
  AGENTS.md

Lanes live in .lane/trees/ inside the repository and are excluded through .git/info/exclude, so nothing is committed.

When things go wrong

trunk has diverged — someone else pushed. git pull --rebase on trunk, then lane merge again.

Rebase conflict — resolve in the lane, git rebase --continue, then rerun lane merge. New notes receive their first baseline only after the rebase succeeds.

lane has uncommitted changes — commit or stash first; the rebase refuses tracked changes either way. Untracked files are fine and need no stashing.

main has uncommitted changes — clean the named tracked files in the main worktree; commit or stash there first. Nothing in the lane was touched.

another lane is landing; try again — another landing or trunk-side audit holds the memory lock. It exits immediately rather than waiting; rerun the command after that operation finishes.

A note is simply wrong — run lane note retire <id> and commit the move to the attic.