pi-checkout-guard
Ambient session presence + peer-aware safety guard for shared git checkouts: blocks destructive git ops while other agent sessions are live, and stops edits escaping isolated worktrees.
Package details
Install pi-checkout-guard from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-checkout-guard- Package
pi-checkout-guard- Version
0.1.4- Published
- Nov 1, 2026
- Downloads
- 386/mo · 386/wk
- Author
- jyyyeung
- License
- MIT
- Types
- extension, skill
- Size
- 83.5 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"skills": [
"./skills/checkout-guard"
],
"extensions": [
"./extensions/checkout-guard.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-checkout-guard
Ambient session presence + peer-aware safety guard for shared git checkouts.
Git gives a checkout one HEAD and one index. Run several agent sessions in
the same checkout and a git reset --hard in one of them silently destroys
every other session's uncommitted work, git add -A sweeps foreign WIP into
your commit, and a session that dies mid-rebase leaves a trap the next session
commits into. The incidents behind this design include irrecoverable WIP wipes
and a commit landing on a detached HEAD left by a crashed rebase 22 hours
earlier.
pi-checkout-guard makes those sessions visible to each other and grades the
dangerous operations accordingly. It is ambient: presence is registered
automatically at session start — no agent cooperation, no manual lock acquire.
session A (shared root) session B (shared root)
│ git reset --hard │ editing src/auth.ts
└── BLOCKED: "1 other live session: │
B — src/ — active 4m ago. │
Message B first, or prefix │
CHECKOUT_GUARD_OVERRIDE=1" │
Install it if you run more than one agent session — or one agent and one human editor — in the same checkout.
Install
pi install npm:pi-checkout-guard
Then restart pi (or /reload). Works with zero configuration in any plain
git repository.
From a local checkout:
pi install ./pi-checkout-guard
pi -e ./pi-checkout-guard/extensions/checkout-guard.ts
Workspace discovery runs lazily from the session cwd on every tool call, so
starting pi outside a repository and then working inside one is not a dead end.
The bash and powershell tools are both guarded. Drop a
.pi-checkout-guard.json at a directory to declare a multi-repo workspace:
all child repos and worktrees under it become one coordination scope.
Quick start
Start two sessions in the same checkout. In the second one, ask for something destructive:
pi-checkout-guard: `git reset --hard` discards ALL uncommitted changes at the
shared checkout (peers' WIP included, even sessions that never registered).
Recover via git-reflog/reflog of the stash, or reset inside your own worktree
while 1 other live session(s) are registered here:
- fix-auth (shared) — /repo — active 4m ago
Message the session(s) listed above before proceeding.
If nobody is affected, re-run with CHECKOUT_GUARD_OVERRIDE=1 prefixed to the
same command.
(Wrapped here for reading; the block reason arrives as a few long lines.)
The blocked command never ran. Check who is there at any time:
> /sessions
Commands
| Command | What it does |
|---|---|
/sessions |
Live sessions (name, where, how long ago they were seen), recent claims, and whether the registry is damaged. |
/sessions claim <text> |
Declare what this session is working on, so peers see it. |
What it does
| Layer | Behavior |
|---|---|
| Presence registry | Every session appends {name, pid, cwd, kind} (kind is shared or isolated, plus ref for a worktree/lane) to one JSONL registry (inside the git common dir — shared across worktrees, never committed). Crashed sessions drop out via pid liveness: the extension runs inside the agent process, so process.pid is the session pid. No ghost windows. |
| Git guard (peer-aware) | Destructive verbs (reset, checkout <ref>, restore, stash, clean -f, rebase, merge, branch -d/-D/--delete, `worktree remove |
| Tree-scoped exemption | A git op whose execution dir resolves inside an isolated worktree/lane is never blocked — not by the graded classes, the peer gate, or the in-flight gate: it is branch-local by construction. This covers a session cwd inside the tree, a leading cd <worktree> && prefix (which overrides the tool's declared working dir), and git -C <worktree> <verb>. Ops resolving into the shared root keep the full guard. |
| Unconditional blocks | reset --hard/--keep and commit --amend at the shared root are blocked even with no live peers — the victim is uncommitted state on disk, which no registry can see. |
| Broad-add guard | git add -A / --all / . at the shared root is blocked while peers are live (their uncommitted files get swept into your commit). Path-scoped adds pass. |
| In-flight gate | Git-mutating verbs are blocked while a rebase/merge/cherry-pick/revert/bisect is mid-flight (rebase-merge/, MERGE_HEAD, …) — liveness-independent, because a session that died mid-rebase looks exactly like "no session". The way out is never blocked: `git rebase --abort |
| Edit containment | Sessions inside an isolated worktree/lane cannot edit outside it; shared-root sessions get a warn-once tripwire when they edit into someone's isolated tree. The warn is owner-aware (see below): silent when the tree belongs to this session, names the live owner when one is working inside the tree, generic verify-ownership hint otherwise. |
| Read-only hygiene | Commands that merely mention a verb (grep 'git clean -fd' log) pass; merge-base, stash list, checkout-index never matched (false positives train agents to ignore guards). |
When you are alone
The single most common reason to uninstall a guard is a block on a solo session, so be explicit about what a solo session gets:
| Command (no other live session) | Result |
|---|---|
git push, git commit, git merge, git checkout <ref>, git stash, git clean -fd |
allowed — peer-scoped rules with no peers have nothing to protect |
git reset --hard, git reset --keep, git commit --amend |
blocked — these discard uncommitted state that no registry can see (a crashed session's WIP, a teammate's, your own second checkout). Block messages say so plainly and never refer to sessions that do not exist. |
| anything mid-flight (rebase/merge/cherry-pick state present) | blocked, except the abort/continue/skip verbs that resolve it |
any of the above, prefixed CHECKOUT_GUARD_OVERRIDE=1 |
allowed, once |
If you never run concurrent sessions, this extension is mostly inert: the unconditional three are the only blocks you will ever see, and the override prefix clears each one.
Configuration (.pi-checkout-guard.json)
{
"isolatedMarkers": [".worktree", ".lane"],
"staleMs": 43200000,
"overrideEnv": "CHECKOUT_GUARD_OVERRIDE",
"notifyHint": "Message the session(s) listed above before proceeding.",
"gitGuard": true,
"editGuard": true,
"rootEditWarn": true,
"inFlightGuard": true
}
| Key | Default | Meaning |
|---|---|---|
isolatedMarkers |
[".worktree", ".lane"] |
Dirs carrying these marker files are isolated contexts. Linked git worktrees (.git file — any worktree manager) are detected automatically. |
staleMs |
12h | Registry rows older than this are ignored. |
overrideEnv |
CHECKOUT_GUARD_OVERRIDE |
Prefix a blocked command with <overrideEnv>=1 to override once. |
notifyHint |
see above | Appended to block messages — put your team's coordination channel here. |
gitGuard / editGuard / rootEditWarn / inFlightGuard |
true |
Toggle individual layers. |
registry |
unset → <git common dir>/pi-checkout-guard/registry.jsonl (or <root>/.pi-checkout-guard/registry.jsonl when the config file sits at the root, or when there is no git dir) |
Override the registry file: a path relative to the root, or absolute. |
Message size and token cost
Guard messages stay small on purpose: peer lists are capped at five names,
then collapse to …and N more (/sessions for full list). When more than five
sessions are live, the per-session notifyHint is replaced by a one-line
suggestion to broadcast once (e.g. via your team's intercom channel)
instead of messaging every session before each shared-root git op.
Disable the extension entirely with CHECKOUT_GUARD_DISABLE=1.
GIT_CEILING_DIRECTORIES is honored during workspace discovery (git-compatible),
protecting against stray .git directories high in the tree.
Owner-aware tree warnings
When a shared-root session edits inside an isolated tree, the guard tries to resolve the owner before warning:
- Tree marker file — if any
isolatedMarkersfile in the tree root holdsOWNER_SESSION=<id>and that id matches this session (its name orPI_SESSION_ID, compared tolerantly: exact, or one is a prefix of the other, so short and full session ids both work), the tree is yours and the warn is suppressed silently. Worktree tooling can stamp this key when it creates the tree; an absent or unreadable file is simply skipped. - Live registry peers — a live session whose presence
cwdsits inside the tree is treated as its active owner: the warn still fires, but names them (Owned by <name> (live) — coordinate with them before editing.) so the next step is coordination, not detective work. - Nothing resolvable — the generic verify-ownership warn fires once per tree, exactly as before.
Set rootEditWarn: false to disable the whole path.
Cross-agent: Claude Code adapter
hooks/claude-checkout-guard.mjs reads and writes the same registry
with the same rules, so Claude Code sessions and pi sessions see each
other. Claude hooks are ephemeral processes, so adapter rows use a
heartbeat-refreshed TTL (expireAt) instead of pid liveness — same
trade-off as TTL-style session locks. Wiring snippet is in the hook file
header. The adapter also mirrors the main guard's tree-scoped git-verb
exemption: when a command's execution directory (a leading cd <dir> &&, a
git -C <dir>, or the session cwd itself) resolves inside an isolated tree
— or outside the shared checkout entirely — every git-verb gate (in-flight,
peer-aware, graded) is skipped for that op, so Claude Code sessions driving
their own worktrees are never false-blocked; commands resolving at the
shared root keep the full guard.
When the registry is damaged
One unreadable line used to collapse the whole file to "no peers" — the guard
then treated a busy checkout as empty, which is the worst possible failure
mode for a safety tool. Parsing is now per line: damaged lines are counted,
and a destructive git op is blocked with the file path and a repair
instruction instead of silently allowed. Read-only commands and the
CHECKOUT_GUARD_OVERRIDE=1 prefix still work, so you are never locked out.
/sessions reports the damage count.
Registry format (spec)
One JSON object per line, append-only:
{"ts":1730000000000,"type":"presence","name":"alice","pid":4242,"cwd":"/repo","kind":"shared"}
{"ts":1730000000000,"type":"presence","name":"w1","pid":4343,"cwd":"/repo/.worktree/w1","kind":"isolated","ref":"w1"}
{"ts":1730000000000,"type":"presence","name":"claude-x","cwd":"/repo","kind":"shared","expireAt":1730000300000}
{"ts":1730000100000,"type":"claim","name":"alice","pid":4242,"claim":"fixing auth flow"}
presencerows are live whiletsis fresh and eitherpidis a live process or, for rows written by a TTL adapter (nopid),expireAtis still in the future. A row with neither is ignored.claimrows are what/sessionsprints underRecent claims— the latest five, dropped once older thanstaleMs.- Third-party tools may append rows following this shape; unknown fields are ignored.
How this differs from neighbors
| peer-aware | liveness | worktree-aware | override | harness | |
|---|---|---|---|---|---|
| pi-checkout-guard | yes, ambient | true pid (in-process) + TTL (adapter) | isolation both directions + in-flight gate | per-command env | pi + Claude adapter |
| static safety nets (e.g. cc-safety-net) | no | n/a | no | policy | many CLIs |
| TTL session locks (e.g. session-lock-style hooks) | yes | heartbeat TTL (ghost window) | lock scope only | per-command | Claude Code |
| manual locks (e.g. agent-lock) | yes | manual acquire/release | per-path claims | manual | any CLI |
Design credit: the in-flight-operation gate and the multi-session lock-map idea were pioneered by Claude Code community hooks (session-lock lineage); pi-checkout-guard adds true pid liveness, incident-graded severity, edit containment, and a documented cross-agent registry format.
Scope and non-goals
- Not a sandbox. It grades git operations and edits into isolated trees. It
does not prevent
rm -rf, and it is not a permission system (use pi-permission-system for tool/path policy). - Not a lock manager. No per-file claims, no acquire/release dance; the registry is presence, and presence is automatic.
- Not a merge resolver. It refuses the operations that destroy work; it never tries to merge two sessions' state.
- Peers must be visible. Sessions in a different checkout, on another machine, or in a terminal where the extension is not loaded cannot be protected — the registry only knows what joined it.
Requirements
| Requirement | Needed for | If missing |
|---|---|---|
| pi ≥ 1.3 | everything | the extension does not load |
| Node ≥ 20 (22.6+ to run the tests) | everything | same |
| git | everything | workspace discovery finds nothing; the guard stays passive |
| Claude Code | the optional adapter | pi sessions still see each other |
No network access, no telemetry, no runtime dependencies beyond pi's own packages.
Troubleshooting
Blocked with no other sessions listed. That is the unconditional class —
reset --hard, reset --keep, commit --amend at the shared root — and it
means the state being discarded cannot be verified by anyone (a crashed
session's WIP still looks like nobody's). Override with
CHECKOUT_GUARD_OVERRIDE=1 when you know it is yours.
Blocked and the message mentions unparsable registry lines. Something wrote
a broken row into the registry. Peer state is unknown, so the guard fails
closed. Delete the registry file (/sessions prints the path); it rebuilds as
sessions re-register.
Can't finish or abort a rebase. You can: rebase --continue, --abort,
--skip and merge --abort are never gated. If you wrote
git rebase --abort; git reset --hard on one line, the second verb is still
graded — split the line.
Nothing ever blocks. Check CHECKOUT_GUARD_DISABLE is unset, that you are
in a repository, and that /sessions lists your peers. Sessions in different
checkouts are invisible to each other by design.
A worktree session gets blocked. It should not: ops resolving inside a
marked isolated tree skip every git gate. Either the tree is not marked
(.worktree/.lane file, or a linked worktree with a .git file) or the
command resolves to the shared root.
Related packages
Five extensions for the same problem — agent sessions sharing a checkout — covering different moments:
| Package | Moment | Guards against |
|---|---|---|
| pi-rc-gate | at execution | the failure signal being destroyed, so a red command is reported green |
| pi-checkout-guard (this) | during | concurrent sessions wiping each other's uncommitted work |
| pi-closeout | at session end | the dirty state a session leaves for the next one |
| pi-retro | weekly | friction that keeps recurring because nobody wrote it down |
| pi-handoff | between sessions | context lost when a session ends |
Development
npm install
npm run typecheck
npm test
License
MIT © jyyyeung