Locking
Select lines exactly as you would to comment on them, and press l. Those lines are frozen, and l again on locked lines lifts the lock.
l writes to the lock file the moment you press it — the marker appears in the gutter straight away and survives leaving the review without submitting. A lock is a decision about the document rather than a comment on it, so it does not queue up behind a submit that may never come.
Because a locked passage is settled, the review will not take feedback on one: f is dropped from the hints and refuses if pressed. Unlock it first, then say what you wanted to say.
The rollout section carries lock L1. The ⚿ in the gutter is what a frozen line looks like.
click to use your keyboard, or tap the keys below
Enforcement is in the CLI, not in the prompt
This is the critical design decision.
Locks have to hold even in bypass-permissions mode, which rules out enforcement by instruction — a prompt is advice, and an unattended agent will eventually ignore it.
So planx capture refuses to write a version that mutates a locked region. It exits non-zero and prints the offending diff. This is the one part of planx you cannot try from the frame above, because it happens on the agent's side — so walk it here:
- 1 · the agent captures
- 2 · the write is refused
- 3 · it asks, or it keeps the block
$ planx capture --plan-id guard-clock-a3f9 --parent v3 --stdin < plan.md The agent has rewritten the rollout section. Lock L1 covers it.
The agent physically cannot land the change. It has one path forward: ask.
Two consequences worth internalising:
- An agent will hit a hard failure mid-revision. That is the design working, not a bug — which is why the error message is part of the product and ends with the exact command that unblocks it.
- Nothing is written on rejection. The store is byte-identical afterwards, so the agent can fix its output and re-run safely.
What locks are not
Locks are an integrity mechanism against agent drift, not a security boundary against a hostile agent. Anything with shell access can edit ~/.planx directly. See SECURITY.md.
What counts as a modification
The lock's stored text must appear, verbatim, in the new version.
- Rewording it — rejected.
- Changing leading or interior whitespace — rejected.
- Deleting it — rejected. Deletion is a modification, by the same path.
- Trailing whitespace on a line — allowed. That is the only normalization, and it exists because editors add it silently.
- Duplicating it so the text now appears twice — rejected, as ambiguous. planx will not guess which copy is the locked one.
Lifting a lock
A rejected capture stops the agent. It has to come back and explain itself: what the block says now, what it wants it to say, and why. Only once you agree does it run:
planx unlock <id> L2 --reason "the flag adds no value for a guard this cheap"A grant is single-use and scoped to one lock. It authorises exactly one capture that may modify L2, then the lock re-arms against the new content. No blanket unlocks, no drift. A second edit to the same block needs asking again.
There is no matching --deny, because nothing is blocked waiting for one. If you say no, the command simply never runs.
This makes locks advisory, not enforced
The agent issues that unlock itself. Nothing verifies that you agreed, or that it asked at all — an agent that decides its reason is good enough can open any lock in the store. Locks stop accidental rewriting, which is the failure that actually happens, not determined rewriting.
What holds it accountable is the record. The stated reason is written onto the grant rather than printed and discarded, so every unlock is visible after the fact:
planx locks <id>If an unlock appears there that you do not remember agreeing to, that is the signal. See SECURITY.md.
Approval seals the entire plan
When you approve a version, planx locks every line of it: one lock per ## section, plus one for any preamble above the first heading.
Per-section rather than one document-wide lock, because it reuses every piece of machinery that already exists — the unlock handshake names a lock, the TUI shows locks in the gutter, and --skeleton collapses them individually. A single monolithic lock would need its own special case for all three.
A plan with no ## headings at all seals as one block.
Sealing skips lines that are already locked, so a block you froze by hand inside a section keeps its own record and its own id rather than being buried under a section lock laid over the top of it.
After approval you can still open the plan and select lines to unlock. Carving a hole in a sealed plan is a normal, supported operation — it just has to be your explicit act.
Locks never overlap
At most one lock covers any given line. Locking a span that is already half locked adds records only for the other half, and the review says so:
locked lines 7–10 as L3 · 4–6 were already lockedPressing l on something half locked still ends up with it locked — that is what the toggle means — but it does not write a second record over the first. Being disjoint is what makes the gutter's lock id unambiguous, keeps planx locks from listing the same text twice, and means an unlock has exactly one record to split.
Adjacent locks are not merged into one. Two blocks locked separately stay separate, so a grant issued against one keeps naming the lines it named when it was issued.
Partial unlock splits a lock
Unlock lines 95–98 of a lock spanning 88–104 and you get two locks (88–94 and 99–104) with the middle free. The alternative — refusing partial unlocks — would force you to unfreeze a whole section to change one line.
The leading fragment keeps the original lock id, so an outstanding grant against it still resolves to something meaningful.
Lock records
// ~/.planx/plans/<id>/locks.json
{
"sealed_at": "2026-08-02T00:14:03Z", // set on approval; null before
"locks": {
"L2": {
"created": "2026-08-01T23:40:11Z",
"origin": "user", // "user" | "seal"
"section": "## Rollout",
"sha256": "c41b…", // of the normalized locked lines
"context_sha": "9f2c…", // disambiguates a repeated block
"text": "…", // verbatim, so it can be re-spliced
"first_locked_version": 2,
"still_present_in": 3,
"consumed_grant": null
}
}
}Locks live at the plan level and carry forward across versions automatically.
Repeated text
If a lock's text appears twice in a plan, planx picks the occurrence whose surrounding lines match context_sha. If that still cannot break the tie, the capture is rejected as ambiguous rather than guessed at — locking the wrong half of a document because two sections read alike is worse than making you look.