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.

Packages

Package details

extensionskill

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

npm CI License: MIT Pi Node

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