pi-rc-gate
Exit-code honesty for coding agents: warns when piped git verbs, || true swallows, or late $? reads would let a failed command be reported as success — plus a static shell lint and a verification-before-claiming skill.
Package details
Install pi-rc-gate from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-rc-gate- Package
pi-rc-gate- Version
0.1.1- Published
- Nov 1, 2026
- Downloads
- 386/mo · 386/wk
- Author
- jyyyeung
- License
- MIT
- Types
- extension, skill
- Size
- 30.5 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"skills": [
"./skills/rc-gate"
],
"extensions": [
"./extensions/rc-gate.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-rc-gate
Exit-code honesty for coding agents: catch the false green before it is reported.
Agents run state-changing commands in shells, then report results. Three shell idioms silently destroy the failure signal — and every one of them has produced real "done, tests pass" claims over failed commands:
git push origin main 2>&1 | tail -5 # pipeline rc = tail's rc = 0
git push -q origin main || true # failure discarded, loop "succeeds"
git push -q origin main; echo done; echo "rc=$?" # $? is echo's status: 0
pi-rc-gate recognizes these as they are typed and warns before the agent reports results — the timing that breaks the false-green habit. Install it if you have ever had an agent say "pushed" while the branch was unchanged.
Install
pi install npm:pi-rc-gate
Then restart pi (or /reload). No configuration.
From a local checkout:
pi install ./pi-rc-gate
pi -e ./pi-rc-gate/extensions/rc-gate.ts
Quick start
Nothing to set up. Keep working — the warn arrives as a steer (a message the agent sees before its next turn, not a block), so the command still runs:
pi-rc-gate WARN (not blocked): this command can produce a false green.
rc-gate [pipe-loss]: state-changing git verb piped into a consumer
(tail/head/grep/…): the pipeline exit code is the CONSUMER's, so a failed
push/commit/merge reads as success. Capture rc first (`git push …; rc=$?`) or
drop the pipe.
Canonical shape: capture rc BEFORE any other statement — `git push -q origin
main; rc=$?; echo "push rc=$rc"; [ "$rc" -eq 0 ] || { echo FAILED; return 1; }`
(escape a deliberate pipe with a trailing `# ok-piped-git` comment)
(Wrapped here for reading; the steer arrives as one line per paragraph.)
One warn per issue kind per session, so the gate never becomes background noise
you learn to ignore. Warnings cover the bash tool and the powershell tool
(with PowerShell consumer aliases: Select-Object, Select-String, Out-String, …).
The canonical shape
git push -q origin main; rc=$?; echo "push rc=$rc"
[ "$rc" -eq 0 ] || { echo FAILED; return 1; }
Between the command and $? there must be nothing — one echo in between
and $? holds the echo's status.
What it checks
| Class | Shape | Why it lies |
|---|---|---|
| Pipe loss | git <state-changing verb> … | tail/head/grep/… |
The pipeline's status is the last command's. A failed push piped into tail exits 0. |
| Swallow | git <verb> … || true (or || :) |
The failure is replaced by success; a retry loop can never fail. |
| Late read | git <verb> … ; other-command; echo $? |
$? belongs to the last intervening command, not the git op. |
Only state-changing verbs count — push, commit, merge, rebase,
reset, checkout, switch, stash (bare), pull, tag (creating),
cherry-pick, revert, apply, am, worktree add|remove, branch -d/-D,
clean. fetch and clone are deliberately absent: they leave the branch and
worktree you are standing on untouched, so a lost exit code there cannot fake a
green write. Read-only forms are exempt for the same reason — a warn layer only
survives if it stays quiet where the exit code cannot lie.
What is not flagged
One verb table drives both the pipe check and the swallow check, so those two
cannot drift apart. scripts/rc-gate.sh expresses the same table in bash (it
has to run without Node), keeps a "keep in sync" note, and is covered by its own
test suite against the same cases:
| Read-only form | Why exempt |
|---|---|
git tag, git tag -l/-n/--contains/--points-at/--format=… |
lists refs |
git stash list, git stash show |
reads |
git apply --check/--stat/--numstat/--summary |
inspects a patch |
git commit --dry-run, git push --dry-run |
reports, sends nothing |
git clean -n, git clean -nd, git clean --dry-run |
lists |
The state-changing spelling of the same verb still fires: git stash (bare =
push), git stash pop, git apply p.diff, git clean -fd. And
git checkout --dry-run fires too — that flag does not exist in git, so the
command fails, which is precisely the rc you need to see.
Deliberate, reviewed pipes are marked with a trailing # ok-piped-git comment,
in shell commands and in script lines alike.
Static lint (scripts/rc-gate.sh)
The same three classes, over shell files, for pre-commit and CI. No args = the
staged *.sh files. It runs on stock macOS bash 3.2 (no mapfile, no bash-4
arrays) and keeps line numbers, so a finding points at the line that lies.
| Exit | Meaning |
|---|---|
| 0 | clean |
| 1 | findings |
| 2 | usage / not a git repo / no such file |
| 3 | internal error |
A broken linter is never reported as a dirty script: infrastructure failures get their own code instead of masquerading as findings.
bash scripts/rc-gate.sh # staged shell files
bash scripts/rc-gate.sh run.sh ci.sh # explicit files
echo $? # 0 clean · 1 findings · 2/3 tool problem
Pre-commit wiring
# .pre-commit-config.yaml
- repo: local
hooks:
- id: rc-gate
name: rc-gate (exit-code honesty)
entry: bash /path/to/rc-gate.sh
language: system
files: \.(sh|bash)$
The skill
The extension warns; the bundled rc-gate skill is the protocol behind the
warnings, for the cases no static check can see: capture rc before any other
statement, verify the claimed effect on disk (git ls-remote after a push,
git log -1 after a commit, read the file back), and treat a child agent's
prose as a claim rather than evidence.
Scope and non-goals
- Not a shell linter. Shellcheck covers syntax and quoting. This knows one thing shellcheck cannot: that piping a git push specifically converts its failure into success.
- Not a blocker. Warnings only, never a block — a guard that can stop your pipeline gets uninstalled the first time it is wrong.
- Not a verification engine. It cannot know whether a push landed; it makes the agent notice that it did not check.
- Not exhaustive over dynamic shells.
bash -c "$(...)"and other indirection are out of reach by construction; only text the tool call actually contains is classified.
Requirements
| Requirement | Needed for | If missing |
|---|---|---|
| pi ≥ 1.3 | the runtime warn layer | the extension does not load; the script and skill still work |
| Node ≥ 20 (22.6+ to run the tests) | the runtime warn layer | same |
| bash 3.2+ (any macOS/Linux shell) | scripts/rc-gate.sh |
— |
No network access, no telemetry, no runtime dependencies beyond pi's own packages.
Troubleshooting
It warned on a command whose exit code was fine. Correct behaviour for the
shape, not for the intent — that is the point. Mark the line # ok-piped-git
and it stays silent forever; read-only verbs are exempted in the classifier
itself, so git stash list | head never warns.
It only warned once. By design: one steer per issue kind per session. Use
/new (or a fresh session) if you want the demonstration again.
The warn did not appear at all. Check RC_GATE_DISABLE — it must be
exactly 1 to switch the layer off; any other value (true, yes, empty)
leaves the gate on. The script layer is independent of it.
rc-gate.sh printed nothing and exited 2. That is "no staged shell files"
or a path that does not exist, not a clean pass. Run it with explicit paths.
Related packages
Five extensions for the same problem — agent sessions sharing a checkout — covering different moments:
| Package | Moment | Guards against |
|---|---|---|
| pi-rc-gate (this) | at execution | the failure signal being destroyed, so a red command is reported green |
| pi-checkout-guard | 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