Capturing Decisions

The reason a change had to be made, written in the commit you were already writing.

What changes for you

Today the reason gets typed twice, in two places, at the same moment:

$ lane note add src/auth.rs -a "fn verify" "early return leaks token length"
$ git commit -am "make verify constant-time"

Both lines are true and both are worth keeping. The first is the one you forget: it takes a separate decision to run a separate command, at the moment you are already thinking about the commit.

Install the hooks

Once per repository, and it is the only setup step:

$ lane install hooks
installed .git/hooks/post-commit
installed .git/hooks/prepare-commit-msg

That covers every lane, because worktrees share the hooks directory. Nothing before it reads a Why: trailer — without the hooks, git carries the line in the commit message forever and lane never sees it.

A hook is a copy, not a link. lane check and lane init name one an older release wrote:

$ lane check
warning: .git/hooks/post-commit is out of date; run `lane install hooks`

What the hooks give you

One command where there were two:

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

post-commit reads the trailer, appends it to .git/lane/pending.jsonl, and the next audit promotes it. From that point it is indistinguishable from a note made by hand:

$ lane why src/auth.rs

[fn verify]
  - 01M0B9MBYB · 2026-08-19
    early return leaks token length

Hand the line to an agent

Writing the Why: line is still yours to remember. lane install skill gives that part away:

$ lane install skill
installed /w/proj/.agents/skills/lane/SKILL.md

The skill carries the trailer format and is loaded only when an agent is doing lane work, so an agent that has just changed a function writes the line into the commit it was already making. The same hooks capture it, and the note that comes out is the same note.

The two sentences are different

That is the whole idea. The subject says what you did. The trailer says what must stay true.

make verify constant-time                      ← what changed, once
Why: ... | early return leaks token length     ← why it must stay that way

Six months later the first sentence is history and the second is still load-bearing.

What it refuses

No targetWhy: refactor the parser is rejected. You must name a file and anchor. This is the filter that does the real work: a commit summary has no target, so it cannot be written in this syntax. There is no discipline to maintain.

A pasted subject — if the trailer text mostly repeats the commit subject, it is refused with a note about what the field is for.

A repeat — the same text on the same anchor is one note, however many commits or amends carry it.

In all three cases the commit still succeeds. A hook that fails your commit over a malformed note is worse than losing the note.

Both, not either

lane note does not go away. The two are producers for the same file:

git commit  →  Why: trailer  →  post-commit hook  ┐
                                                  ├─→  .git/lane/pending.jsonl  →  audit  →  note
lane note add … -a … "…"  ──────────────────────  ┘

Nothing downstream knows which one a note came from. Same promotion, same fingerprinting, same budget.

Overlap is safe. Promotion drops a pending record whose text already matches a live note on the same anchor, so writing a Why: trailer and running lane note with the same sentence gives you one note, not two.

Reach for the trailer when the decision and the change happen together, which is most of the time. Reach for lane note when they do not:

  • you learned something reading code you are not changing
  • the insight arrives mid-work, nowhere near a commit boundary
  • the note is long, or awkward to type inside a commit message

The only rule that differs is the pasted-subject check, and only because it cannot apply: lane note has no commit subject to paste.

What it will not do

It reads one field. Not your diff, not the commit body, not commits without trailers. There is no stream being filtered — if you never write a Why: line, nothing is ever captured. That is the answer to importing the git log: the mechanism has no input except the sentence you chose to write.

One side benefit

Because it is a real git trailer, the reason also stays in git history. git log --grep, git interpret-trailers, and GitHub’s UI all see it. The decision ends up recorded in two places that serve different readers — git log for someone reading history, .lane/ for someone about to edit the function.

What is automatic and what is not

Automatic, once lane install hooks has run: the hook firing, parsing, validation, the append to .git/lane/pending.jsonl, promotion at the next audit, and deduplication. You never run any of it.

Not automatic: deciding there is a Why: line to write. That is deliberate. A note generated from a commit that nobody chose to annotate would be the git log again, which is the thing this design refuses. An agent with the lane skill writes the line because it judged the constraint worth recording — the same judgment, made by someone else.

So the only thing you supply is one line, in this shape:

Why: <path>[#<anchor>] | <text>

<anchor> is optional and defaults to @file.

You do not have to memorise it. A prepare-commit-msg hook puts the form in front of you as a comment whenever an editor opens:

# Why: <path>#<anchor> | what must stay true (optional, records a lane note)

Git strips comment lines from editor commits, so it never reaches the stored message. The hint is skipped entirely for git commit -m, where comments are not stripped and would pollute the message.

The honest cost

You are still deciding,at each commit, whether anything here is worth keeping. Nothing makes that judgement for you.

What makes it likely to stick is timing rather than ergonomics: you are already writing a commit message, so the prompt to explain yourself is already on screen. lane note asks you to have that thought at a moment when nothing is asking you for it.