Release v0.9.1#207
Merged
Merged
Conversation
…g (ob-y5ep) (#191) The audit logger wrote command lines through `scanner.redact()`, which only masked substrings matching a known secret *pattern*. A command run as `RAFTER_API_KEY=<value> rafter ...` logged the value in plaintext: the key's shape matches no built-in pattern, so `redact()` left it untouched — leaking real API keys into `~/.rafter/audit.jsonl` despite `redact_secrets: true`. Add env-assignment redaction to the shared redaction path (`PatternEngine.redactText` / `redact_text`), so every `scanner.redact(command)` call site benefits (both `logCommandIntercepted` and `logPolicyOverride`). Any `NAME=VALUE` token whose NAME looks secret-bearing (`/(?:^|_)(KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|PWD|API[_-]?KEY|ACCESS[_-]?KEY|CREDENTIALS?|AUTH)$/i`, which includes RAFTER_API_KEY) has its VALUE masked with the codebase's existing char-mask. This is additive to the existing pattern-based redaction — it catches values that match no pattern but sit behind a secret-named env var. Benign assignments like `FOO=bar` and `NODE_ENV=production` are left untouched. Both implementations updated in lockstep (identical regex, ordering, and mask). Tests added in both suites: a `RAFTER_API_KEY=<val> rafter scan .` command must not leak the raw value into the log, and benign assignments must be preserved. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ow-up to #190) (#194) Follow-up polish on @Minh-Nguyen-2k7's Mailchimp pattern (#190): asserts a bare 32-char hex with no -us datacenter suffix does NOT match, in both Node and Python, and removes trailing whitespace on the Python test. Test-only; pattern unchanged. Verified: node secret-patterns 58 passed, python test_regex_scanner 16 passed. Co-authored-by: Minh-Nguyen-2k7 <Minh-Nguyen-2k7@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#195) The rafter skill prompts were all-escalation with no scoping ("DO NOT stop at local", "an un-evaluated 'done' is not done", "default: run rafter run") and never said WHAT counts as security-relevant or when the gate does NOT apply. Agents writing research/experimental code with zero security surface (training scripts, data analysis, plotting, model eval, notebooks, pure computation over trusted local data) kept getting the gate in the way. Fix (prompting only — no executable code changes): add a "When this applies (and when it doesn't)" framing driven by the security SURFACE of the diff, not the task's label. - ENGAGE fully (unchanged, not weakened): auth, credentials/secrets/ tokens/sessions, user/untrusted input, SQL, shell/exec, file paths + uploads, deserialization, crypto, network-facing endpoints, data deletion, dependency/manifest changes. - BACK OFF: research/experimental/local-only/throwaway code with NONE of that surface — a quick surface check suffices, and with genuinely no surface it is fine to proceed without the full rafter-code-review + rafter run. - Key rule landed in every edited skill: judge by the actual security surface of the diff, not by whether the work is called "research." Research code that touches secrets/shell/network/untrusted input still fully engages. Also surface-conditioned the overly-broad `description:` front-matter (that string drives auto-invocation). Files: node/resources/skills/{rafter,rafter-code-review, rafter-secure-design}/SKILL.md and their byte-identical python mirrors under python/rafter_cli/resources/skills/. rafter-skill-review is left unchanged on purpose: vetting third-party executable context before install is always a genuine security surface, so a back-off there would weaken a real gate. This REDUCES false engagement on no-surface code without removing or softening protection for any genuine security surface. Refs: sable-hjd3 AI-assisted (per repo AI Policy): authored with Claude Opus 4.8. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…el (sable-m9n6) (#196) `rafter agent init` injects RAFTER_INSTRUCTION_BLOCK into agents' instruction files (CLAUDE.md, and ~/.codex/AGENTS.md — a GLOBAL Codex instructions file). The block was a blanket gate ("A security-relevant task is not complete until reviewed... Stop and invoke before continuing") with no scoping. Because Codex reads ~/.codex/AGENTS.md as global instructions, every Codex session was ordered to treat its task as rafter-gated — agents went to read rafter's SKILL.md instead of doing the requested (often no-surface, research) work. Instruction-level hijacking. This applies the same surface-based scoping that PR #195 gave the skill prompts, to the injected block itself, so block and skills tell the same story: - Engage the gate when the change touches a real security surface (auth, secrets/ tokens, untrusted input, SQL/query, shell/exec, file paths, deserialization, crypto, network endpoints/SSRF, data deletion, dependencies). - Back off when NONE of those are present — research/experimental/local-only/ throwaway code (training scripts, analysis, plotting, model eval, notebooks, pure computation over trusted local data): a quick surface check is enough. - Decisive rule: judge by the diff's actual surface, not the "research" label — research code that reads a secret, shells out, hits the network, parses untrusted input, or bumps a dependency still gets the full gate. Guardrail: this reduces false engagement on no-surface code without removing or softening protection for any listed surface — the routing bullets were expanded (crypto, network/SSRF, data deletion, dependencies) so coverage strengthens, not weakens. node and python RAFTER_INSTRUCTION_BLOCK remain byte-identical. Also (sable-h0ah): narrow the Codex .codex/hooks.json PostToolUse matcher from `.*` to `Bash|apply_patch` so posttool stops firing on every Read/MCP call (log noise + latency), matching Codex's own PreToolUse surface and the claude-code posttool scoping. Applied in init.ts, components.ts, and the python mirror; the one test asserting the old value is updated. AI-assisted (Claude Opus 4.8) per repo AI Policy. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… PostToolUse matcher (#197) The Python dry-run preview still printed 'PostToolUse: .*' after #196 narrowed the actual Codex PostToolUse matcher to Bash|apply_patch (Node's equivalent plan string was updated; Python's was missed). Cosmetic parity fix — the installed matcher was already correct; only the printed plan was stale. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… stale poetry.lock (#198) Adds an explicit cryptography constraint to pin the in-major fix for GHSA-537c-gmf6-5ccf (held to 46.x to avoid the unrelated 49.x jump). While regenerating, found poetry.lock was STALE and violated its own pyproject constraints — the previously-declared Python security bumps were never locked, so the vulnerable versions were still what actually installed: python-dotenv ^1.2.2 -> lock had 1.0.1 (now 1.2.2) requests ^2.33.0 -> lock had 2.32.4 (now 2.34.2) urllib3 >=2.7.0,<3 -> lock had 2.6.3 (now 2.7.0) cryptography (new pin) -> lock had 46.0.5 (now 46.0.7) lock-version 2.1 preserved (regenerated with Poetry 2.4.1). `poetry check --lock` passes. Python suite: 1495 passed against the new locked deps. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#195 added the surface-scoping section and grew the rafter router SKILL.md from 118 -> 142 lines, blowing BOTH line budgets (node <120, python <130). With no CI, those two tests have been red on main since it merged. - Condense SKILL.md 142 -> 128 lines: the 11-bullet engage list becomes a compact inline list (every surface kept) and the tier section is tightened. No content dropped — the scoping rule stays in the router, since it decides whether the skill is entered at all and can't be pushed into a sub-doc. - Align the node budget 120 -> 130 to match python's. The limits had drifted: python was bumped 120 -> 130 for the scanner-scope section; node never was. Tests: node brief.test.ts 28 passed; python test_brief.py 35 passed. node/python SKILL.md byte-identical. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…as a command (sable-4v6e) (#200) The pretool hook hard-denied `gh pr create --body "…git push --force…"` and `git commit -m "don't git push --force"` because it matched risk/policy patterns against the RAW command line — quoted DATA (a PR body, a commit message) was treated as if it were the command. Two compounding defects: 1. Naive matching over the whole line (command-interceptor `matchesPattern` and risk-rules `assessCommandRisk`) saw inside quoted arguments. 2. A `policy.blockedPatterns` hit hard-coded riskLevel:"critical" — a hard deny that ignored the assessed risk — and the default deny-list held loose literals (the substring "rm -rf /" also denied `rm -rf /tmp/build`, which is only HIGH). Fix — an argument-aware sanitizer (`sanitizeCommandForMatching`, new in both risk-rules.ts and risk_rules.py) that all risk/policy matching now runs against: - Tokenize respecting single/double quotes, escapes, `$(…)`/backticks, redirects; split on chain operators (`;` `&&` `||` `|` `&`), kept in place so pipeline rules (`curl … | bash`) still match. - Redact only spans that are DATA: operands of text commands (echo/printf/grep/…), values of prose flags (`-m`/`--message`/`--body`/`--title`/…), and quoted multi-word prose arguments to ordinary commands. - Preserve and RECURSIVELY sanitize spans that are CODE: a shell's `-c` argument (bash/sh/zsh/…), `$(…)`/backticks outside single quotes, eval-style flags (`-c`/`-e`/`--eval`/`-exec`) and execs (eval/exec/ssh/xargs); prefix wrappers (sudo/env/timeout/nice/…) delegate to the command they wrap. The executable token is unquoted so quoting the command name (`"rm" -rf /`, `watch "rm -rf /"`) can't break the anchors. Recursion depth-capped at 8; fails safe (stays flagged). So `bash -c "rm -rf /"` still hard-blocks (its quoted arg IS a command), while `echo "rm -rf /"` is DATA and allowed. Defect 2: a deny-list match now reports the ASSESSED risk (still allowed:false), and DEFAULT_BLOCKED_PATTERNS is exactly the unconditional CRITICAL_PATTERNS set — so `git push --force` stays HIGH / approval-gated as intended, not silently coerced to a critical hard-deny. Node and Python mirrored (0 divergences across a 50-case differential corpus). Tests both directions in both suites; the old "quoting breaks rm -rf" known-gap tests now assert it's caught. Security: reviewed with the rafter agent (adversarial, ~20k differential-fuzz cases). It confirmed no catastrophic command moved from denied to silently allowed, no Node/Python divergence, and no DoS. It surfaced the exec-name quoting bypass (`"rm" -rf /`) — fixed here with tests — and two pre-existing, out-of-scope pattern-coverage gaps filed as sable-urvj (long-form `rm --recursive --force`) and sable-2t0w (Node audit-logger snake_case config). AI-assisted (Claude Opus 4.8) per the repo AI Policy. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a SendGrid API key detection pattern (SG. prefixed, critical severity) to the built-in regex scanner in both the Node and Python implementations, with positive and negative tests in each suite and a CHANGELOG entry. Mirrors the existing DigitalOcean provider-key pattern. Written with Claude Code. Co-authored-by: Claude <noreply@anthropic.com>
#201: blank line before the new Python SendGrid test; blank line before the ## [0.9.0] CHANGELOG header. #202: add @vitest/coverage-v8 so `test:coverage` works, and restore package.json's trailing newline. Pinned coverage-v8 to 4.1.0 (not ^4.1.0) because it requires an EXACT vitest peer, and vitest is locked at 4.1.0 — a caret pulled 4.1.10 and tripped the peer check. Minimal: no vitest bump. Verified: `vitest run --coverage` produces a v8 report; node secret-patterns and python test_regex_scanner suites green. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a config flag `scan.plus_requires_approval` (OFF by default) that requires explicit confirmation before a paid `rafter run --mode plus` scan spends credits. Motivated by an external user whose AI agent auto-ran a Plus scan and consumed credits without asking. When enabled, a Plus scan (via `rafter run`, `rafter scan`, or `rafter scan remote`) prompts `[y/N]` on a TTY and refuses in a non-interactive/agent context with new exit code 5 unless `--yes`/`-y` or `RAFTER_CONFIRM=1` is present. Fast scans are never gated. Precedence is additive (OR): a project `.rafter.yml` can turn the gate ON but can never turn OFF a gate the machine owner set in global config, so a hostile repo can't silently re-open the credit-burn hole. Fails open (gate off) on a config/policy read error, matching the OFF default. The rafter agent skill and injected instruction block now tell agents Plus is a paid tier and to ask the user before running it. Dual-impl Node + Python with full parity; tests in both suites; CHANGELOG and CLI_SPEC (exit-code matrix, `--yes`, config schema) updated.
Cut v0.9.1 from main → prod. Ships the Plus-scan approval gate (scan.plus_requires_approval, #204) and the SendGrid secret pattern (#201), plus everything accumulated on main since the 0.9.0 tag (OpenCode support, multi-provider remote scan, Mailchimp pattern, hook/audit/skill fixes). Bumps node/package.json, python/pyproject.toml, and both rafter-security-skill.md frontmatters to 0.9.1 (validate-release parity), and rolls CHANGELOG [Unreleased] → [0.9.1].
Raftersecurity
approved these changes
Jul 22, 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→prod, publishing v0.9.1 to npm (@rafter-security/cli) and PyPI (rafter-cli) viapublish.yaml.Headline
scan.plus_requires_approval, feat(scan): opt-in approval gate for paid Plus scans (sable-9ddf) #204) — stops an AI agent from spending a user's credits onrafter run --mode pluswithout explicit confirmation. Off by default; new exit code 5 when refused non-interactively.Also included since 0.9.0
test:watch/test:coverage/cleannpm scripts (Add npm script #202)Version
Bumped to 0.9.1 across node/package.json, python/pyproject.toml, and both
rafter-security-skill.mdfrontmatters (#206); CHANGELOG rolled to[0.9.1] - 2026-07-21.validate-releaseenforces parity on this PR.Verification
AuditLoggerred is a non-hermetic test unrelated to this release, tracked insable-883t); Python suite green.