Skip to content

fix: close the v0.19.0..v0.21.2 audit findings - #768

Merged
orieg merged 35 commits into
mainfrom
fix/audit-v0-21-findings
Oct 16, 2026
Merged

orieg merged 35 commits into
mainfrom
fix/audit-v0-21-findings

Conversation

@orieg

@orieg orieg commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Fixes the defects found by an audit of v0.19.0..v0.21.2. Closes #769. Refs #482.

Hooks and session guards

  • [hooks.verified-stop] witnessed a failing run as passing when the post-tool payload carried a result but no recognised exit-code field (Codex "Exit code: 101\n..." string, Qwen llmContent). A run now counts only when its exit code is stated; any non-zero statement wins.
  • hook install now wires each agent's shell tool to hook run --event post-tool, so the witness path is reachable from installed hooks.
  • In refuse mode, a stop whose refusal cannot be counted (no session id, unwritable store, held sessions/.lock) is refused instead of let through. The loop guards still apply.
  • Session and lock files are created 0600; the docs describe the real lock (a create_new lock file, not flock), the merge-base comparison, the excluded gitignored files and the command sources.
  • Default runners are vouched only when their ecosystem marker is in the base tree.
  • OpenCode plugin: comments now match behaviour, and an undeliverable report goes to stderr instead of being dropped.
  • Release tarballs record owner 0 and fixed modes on both GNU tar and bsdtar.

Exit-code masking

  • gate-command-lint and ci-integrity exit-code-masked read masking from the bash syntax tree through a shared src/guards/shell_masking.rs.
  • They now catch || echo, || /bin/true, ||true, || :, || exit 0, || exit 00, || { true; }, (cmd || true) and set +o errexit.
  • pipefail counts only from a real set command.
  • Text inside strings or comments is ignored.
  • A script that does not parse is named in the notes.

Research gates

  • mechanism-sections:
    • no longer panics (exit 101) on non-ASCII claim text;
    • reads ~~~ fences;
    • keeps prefixed headings in scope;
    • requires (verified: …) targets to be tracked.
  • citation-anchors:
    • new min_quote_chars (default 8) and finding code quote-too-short;
    • a cited commit must be on the base's history;
    • the tracker-title check is tested, counts lines correctly, and reads DISCIPLINE_NO_NETWORK like the forge client;
    • AGENTS.md §3.3 now lists the tracker-title check among the forge-reading features.
  • scope-confinement judges rename sources and symlinks.
  • claim-labels requires a separator and real content, and ignores markers in code spans. Its precision study now states that it is in-sample.

stub-bodies, suppression-delta, unsafe-budget, config-integrity

  • A changed production equality method made constant is reported at gate severity again (v0.19.0 behaviour). Added methods stay capped at warning.
  • Carve-outs are read from syntax nodes instead of source text. Boolean.TRUE and parenthesised constants are recognised.
  • suppression-delta reports rule identifiers only; descriptions and justifications are no longer echoed. It also reads the v8 ignore and node:coverage markers.
  • unsafe-budget fails closed (exit 2) on a head side that does not parse.
  • config-integrity compares the head file's effective enabled state under --disable.
  • GATES.md now states the resolve-locked preset commands as the code runs them.

Bounds and benchmark

  • bounds.rs:
    • quantile tails are pinned;
    • the error bound is stated as relative;
    • the MDD docstring names the unpooled form;
    • sample_size_for_interval errors instead of saturating.
  • The benchmark summary test tallies adjudication.json and recomputes kappa.
  • The script docstring states that the harvest and replay harness is not committed.
  • A post-hoc audit is recorded: deviations, PR-cluster CIs, power, and the comparison twin labelled definitional (C-b). epic: 1.0 evidence for live sessions with Codex CLI, Cursor and Qwen Code, and the public precision benchmark #482 Part B moves from Done to Partial.
  • The locked pre-registration and the results JSON are unchanged.

ROADMAP

  • Ledger rows are added as unreleased.
  • The v0.21.1 preset row now names the four real preset ids.
  • The v0.20.0 equality-made-constant row records the loosening.
  • The live-session row names its committed evidence.

Verification

Each fix carries a test that was watched failing on the v0.21.2 behaviour and passing with the fix; mutation evidence is in the commit messages.

  • RUN: cargo fmt --check, cargo clippy --all-targets -- -D warnings, discipline docs --check and scripts/benchmark_precision.py --test pass on the merged head, and discipline self-test gives 328 cases, 0 failed.
  • RUN: full local cargo test --no-fail-fast on the merged head: 141 test binaries, 4212 passed, 0 failed, 1 ignored.

allow-agent-instructions: AGENTS.md lists the citation-anchors tracker-title check among the forge-reading features (§3.3)
allow-agent-instructions: .claude/settings.json gains the generated post-tool entry for Bash
allow-agent-instructions: .agents/hooks.json gains the generated post-tool entry for run_command
allow-agent-instructions: .github/hooks/discipline.json gains the generated post-tool entry for bash
allow-agent-instructions: .opencode/plugins/discipline.js gains the generated post-tool witness for shell tools
allow-swallow: .opencode/plugins/discipline.js the post-tool witness only records a passing run and must never refuse a tool call
allow-assertion-drop: the_opencode_plugin_registers_every_hook_in_both_blocks the plugin runs two more commands for the post-tool witness
allow-assertion-drop: the_opencode_observe_plugin_has_no_throw_and_guards_every_handler two more try/catch blocks for the post-tool witness
allow-assertion-drop: the_opencode_enforcing_plugin_refuses_on_a_non_zero_exit_and_reports_no_registration the witness try/catch and the dropped-report console.error change the counted forms
allow-assertion-drop: the_opencode_observe_plugin_never_throws_whatever_the_command_does two more post-shell handlers in the driver
allow-assertion-drop: positive_control_changed_equality_made_constant the changed equality method made constant is reported at gate severity again, so the run now exits 1 instead of 0

orieg added 30 commits October 15, 2026 18:58
…ate severity

An existing production equality method whose substantive base body is
replaced by a constant `true` was reported as body-replaced-by-stub at
the gate's severity up to v0.19.0. Since equality-made-constant took
over that case it was capped at warning, so the run passed. A changed
method now keeps the gate's severity again; an added equality method,
one whose base body was already a bare constant, and one in test code
stay capped at warning.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
parse_post_tool_outcome read any payload carrying tool_response, tool_result, toolResult or output without an exit-code field as exit 0. Codex sends tool_response as the string "Exit code: N ..." and Qwen Code puts "Exit Code: N" in tool_response.llmContent, so a failing cargo test was recorded as a passing verification and [hooks.verified-stop] let the stop through.

The exit code is now read from numeric fields, from Exit code: N statements in the text result (any non-zero statement wins, so output that prints a passing line cannot mask a failure), from Copilot's <exited with exit code N>, and for Claude Code from a non-interrupted PostToolUse (documented: a failing tool goes to PostToolUseFailure). llmContent is read as output. An unknown exit code is never witnessed; Copilot resultType success alone no longer counts.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…es, heading prefixes, tracked targets

truncate_claim sliced the claim at byte 57 and panicked (exit 101) when that byte fell inside a multi-byte character; it now counts characters.

A ~~~ fence is a code fence, closed only by the same character at least as many times. A heading that starts with a designated heading (`## Root cause (summary)`, `## 2. Root cause`) stays in scope; `#word` is no longer a heading. A (verified: <path>) target must be tracked in the index: a file only on disk no longer resolves.

The per-document check is now judge_document, which self-test runs with a positive and a negative control. GATES.md gains a What it does NOT catch list.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
The equality-made-constant carve-outs matched source text: a comment
holding `#[derive(` and `PartialEq`, or the words `import mock`
anywhere in the file, exempted a constant equality method. They now
read syntax nodes: mock-framework imports from import statements, a
mock base class from the class's bases, and generator markers from
Rust attribute items and Java annotations.

A hand-written equality method is never generated: @DataClass, attrs,
pydantic BaseModel, Lombok, a Java record and Kotlin data classes leave
a hand-written method in place of the one they would generate, and Rust
refuses a hand-written impl beside a derive. Those markers no longer
exempt it.

A test double in a module named for test doubles (fakes.py, mocks.ts)
is carved out, and one in tests/fakes.py keeps the carve-out when the
file is judged as production code because it holds no test.

Java `return Boolean.TRUE;` and any parenthesised constant
(`return (true);`) are trivial bodies, so the Boolean.TRUE and
parenthesis branches of the constant-true check are reachable. A
self-test case covers a changed method made constant and a generated
one.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
In refuse mode, a stop whose refusal could not be recorded against max_per_session (no session id in the payload, a session store that cannot be written, or .git/discipline/sessions/.lock held, a file the agent can create itself) was let through with exit 0. It is now refused, with a line saying why the refusal was not counted. stop_hook_active and agy executionNum are checked first, so the agent's loop guard still bounds the refusals; observe mode only logs.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…ibe the MDD form

standard_normal_quantile: the docstring called Acklam's 1.15e-9 bound
absolute; it is relative (measured max relative error 1.13e-9 against
scipy.stats.norm.ppf; absolute error up to 2.9e-9 for |z| < 3 and 3.8e-8
near p = 1e-256). The tests pinned only fast-path constants, so a changed
tail coefficient (C[5] 2.938 -> 2.948) survived the suite. New pins at
p = 0.01, 0.99, 1e-6, 1e-12, 0.3, 0.7 (scipy, relative tolerance 2e-9)
kill that mutant in test_standard_normal_quantile_pinned_reference_values.

sample_size_for_interval: a half-width of 1e-300 gave n = inf, cast to
usize::MAX. It now errors when n is not finite or exceeds MAX_SAMPLE_SIZE
(1e9); sample_size_for_interval_refuses_an_unbounded_result fails on the
old code (unwrap_err on Ok) and passes now.

minimum_detectable_difference: the formula is unchanged (the locked
pre-registration pins its values). The docstring now says it solves the
unpooled Wald-type equation, not the Fleiss pooled-null formula, and
states the measured gap (0.91% at n = 141, 0.54% at n = 245, up to about
9% at n = 10).

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…oc audit statistics

The script docstring (and the #766 commit message) described a harvester,
replay runner, rater packet builder and summary generator. None of that is
in the file or anywhere in the repository, so manifest.json, findings.json,
adjudication.json and summary.json cannot be regenerated from committed
code. The docstring now says so.

The sample-size guard of src/guards/perf/bounds.rs is mirrored (Python
raised ZeroDivisionError at w = 1e-300).

Post-hoc audit, not pre-registered (--post-hoc-audit [--write]): Fleiss
pooled-null MDD, the design's pass probability and the n at which a
Wilson lower bound clears 0.90, a Newcombe independent-sample interval for
the structural-versus-twin difference, and PR-clustered intervals for the
structural class (cluster-robust ratio and a PR-cluster bootstrap, seed
20261015, 20000 resamples). Each is pinned in --test against scipy,
statsmodels or numpy values, and post-hoc-audit.json is checked against a
fresh recomputation.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…n the base tree

With [hooks.verified-stop] run_at_stop = true and no command configured, all four defaults (cargo test, pytest, npm test, go test ./...) had to pass, so a Rust-only repository was always refused. The defaults are now the runners whose marker (Cargo.toml; pyproject.toml, setup.py or pytest.ini; package.json; go.mod) is a file at the root of the base tree, so a marker the change adds vouches for nothing. With no vouched command the refusal says so and names the setting.

docs/CONFIGURATION.md now lists where vouched commands come from (hooks.verified-stop.commands, [gates.command], [gates.test-floor] test_command, then the defaults) instead of the [commands.<id>] gates it named.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…labels

test_benchmark_summary_results_match_rust_bounds checked summary.json's
intervals and verdicts against its own counts only. It now joins
findings.json to adjudication.json's final labels, tallies them per gate,
per class (from the locked pre-registration) and for the twin, recomputes
the raters' exact agreement and Cohen's kappa from their individual
labels, and requires summary.json and adjudication.json to match (counts
exactly, kappa to 1e-6). Each summary gate row's class must match the
pre-registration.

Discrimination: with ITEM-0003's three labels flipped consistently from
false-positive to true-positive in adjudication.json, the previous test
file passes (6 passed) and this one fails ("twin: labels give 10 true
positives, summary.json says 9"); the data was restored. The new
flipping_one_adjudication_label_breaks_the_summary_cross_check flips a
discipline final label, a twin final label and one rater label in memory
and asserts each is reported.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
docs/CONFIGURATION.md said session files are written 0600 and flock-locked. The store wrote them with std::fs::write (0644 under the usual umask) and serialises writers with a create_new lock file that is removed as stale after 30 seconds. Session files and the lock file are now created 0600 on Unix (OpenOptions mode, then an atomic rename), and the docs describe the lock file as it is: an ordering between honest writers, not a security boundary.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
… it leaves out

The verified-stop section said a turn is checked when files changed since HEAD; the tree digest diffs the working tree against the merge base of HEAD with the base branch, falling back to HEAD's tree. It now says so, and that files git ignores are excluded from the digest.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
With quote_line = false the label still appended the site's rule text,
and for many packs that text runs past the rules: an ESLint description
after `--`, a nolint or NOLINT comment body, a pylint or noqa trailing
word, a clippy `reason = "..."`, a C# pragma or SuppressMessage
justification, a Scala nowarn `msg=` filter, an annotation string with
a second word. A token written there was echoed in every report format.

The label now holds a fixed tool name and the rule identifiers, each
spelled with letters, digits and _@/.:- only; the list stops at a
description marker, a named argument, a word spelled otherwise, or a
word that follows a rule with no comma (a flake8 code in a noqa
excepted). quote_line = true still quotes the line.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
The unfailable-command check was one regex that matched only a trailing
true, exit 0 or :, so cargo test || echo ok, || /bin/true, (cargo test || true),
|| { true; }, || exit 00 and ||true passed. The pipefail check accepted the text
set -o pipefail anywhere, including inside a quoted string, and only pipes into a
fixed list of filters were checked.

A new shared module (src/guards/shell_masking.rs) parses the command with
tree-sitter-bash and reports: an || list whose right side cannot fail; under a
gate command (errexit off) a last command that cannot fail; set +e with no later
status check while errexit is on; and a pipeline with a failable stage before the
last while pipefail is off, whatever the filter. pipefail and errexit are read only
from real set command nodes. A command that does not parse, or nests deeper than
the analysis reads, gets a could-not-verify note.

gate-command-lint uses it through analyze_gate_command; a self-test case runs it
with positive and negative controls. Tests pin || : and ; : (audit M6).

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…ysis

On GitHub, exit-code-masked was a substring test for '|| true', so ||true,
|| :, || exit 0, || echo failed and || /bin/true passed; on GitLab the list was
'|| true' and '|| :'. Text that only mentioned a mask (a comment, an echo) was
reported. Both paths now use src/guards/shell_masking.rs on the script read with
errexit on, after replacing ${{ }} and $[[ ]] template expressions byte for
byte. The checked set +e exemption is kept (status_checked, shared).

A changed script that does not parse, or a step whose shell is not bash or sh,
gets a could-not-verify note. The finding code and the GitLab directive subject
are unchanged; the GitHub finding is anchored at the masking line.

cmd; true is not reported in a CI script: errexit ends the script at the first
failure before true runs. (audit M7)

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…_chars; test tracker titles

A quoted text with fewer than min_quote_chars non-whitespace characters (new key, default 8) is reported as quote-too-short: a one-character quote was contained in the cited line and verified.

A cited commit must be the base or one of its ancestors (GitCtx::is_reachable_from_base; HEAD when the run has no base). A commit only this change, another branch or the object database holds is reported as commit-unresolvable: evidence committed and deleted again inside one pull request verified before.

The tracker-title check is now covered by tests over a canned forge (match, mismatch, missing issue, offline note, require_online error). Its line number was one short for a reference at the start of a line; lines are now counted in one pass. DISCIPLINE_NO_NETWORK is read as the forge client reads it, so 0 and false no longer switch the check off. The forge is identified once per run.

AGENTS.md 3.3 lists the tracker-title check among the features that read a forge's REST API.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
Vitest's default v8 provider reads `/* v8 ignore next */` and
`/* v8 ignore start */`, and Node's test runner reads
`/* node:coverage ignore next */` and `node:coverage disable`. The
JavaScript pack read only istanbul and c8. The new markers follow the
same rule: they must open the comment, so prose that mentions one and a
string that holds one are not sites.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
The rounding helper was a lambda behind noqa: E731, which suppression-delta flags; it is a nested function now. Output unchanged (--test passes, including the post-hoc-audit.json freshness check).

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…-hoc audit

#482 Part B said "Done" and "human review, blind to the gate", while its
own "Ships when" needs every number to resolve to a committed artifact
and the harvest/replay/sampling/packet code is not committed. The row is
now Partial: numbers resolve to committed JSON, the harness is not
committed, rater provenance is unverified.

Corrections from the data: dependency-delta has 7 vite false positives
(not 6), so the named mechanisms cover 19 of 23; the 43 pii false
positives are 21 RFC 1918 literals, 19 macOS home-directory paths in test
code (ITEM-0334 among them) and 3 placeholder secrets (line text read at
each head commit through the GitHub API). The twin gap is labelled
PASS_categorical_by_design: the twin's winning regime (recall) was not
measured.

New post-hoc audit bullet, labelled not pre-registered: undeclared
deviations (version 0.21.1 vs 0.20.3, a non-existent full evaluated_commit
SHA, ruff-sh/ruff does not exist, 12 non-PR commits against merged-only,
merge-commit head_sha, a wider twin path rule) and the
INTERMEDIATE_amended_threshold_redesign label the pre-registration
prescribes; PR clustering (design effect 6.0, effective n 24.0, bootstrap
and cluster-robust intervals containing 0.90); a Newcombe interval for
the twin difference; unverified rater provenance; design power 0.591 at
n = 141, smallest clearing n 127, Wilson bound 0.8352 for 216/245.
Numbers come from docs/benchmarks/results/post-hoc-audit.json.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
… ci-integrity

gate-command-lint: how a command is read, what cannot fail, the forms caught,
the could-not-verify note, and a What it does NOT catch list. ci-integrity:
exit-code-masked now names the forms read from the script's syntax tree, the
cmd; true case under errexit, and its blind spots.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
Since c469122 a changed file whose head side did not parse was noted
and skipped, so with max_unsafe = 0 a change adding an unsafe block to a
file nested past the parse-depth limit exited 0 once the other
parse-strict gates were off. The budget is a count of every unsafe site,
and a file with no facts cannot be counted: the gate now returns the
parse error, which stops the run with exit 2 (could not check), as it
did before c469122. A base side that does not parse is still noted and
every head-side site is judged as new.

The c469122 tests that expected a note are changed to expect the
could-not-check, and the tree-depth and bounded-parse tests no longer
list unsafe-budget among the gates that read what they can.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…n paths

The gate checked only a changed file's path. git mv secure/policy.py src/ passed while deleting the same file was blocked; a rename is now judged at its source as well, and the finding names where the file moved.

changed_files skips symlinks, so a new symlink under a forbidden directory passed. GitCtx::changed_files_including_symlinks lists them for this gate only; every other gate keeps reading changed_files.

Plan-mode and path judgement are now planned_output_breaches and changed_file_breaches, which self-test runs with positive and negative controls. GATES.md describes plan mode under What it catches.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…er --disable

A gate switched off by --disable was skipped unless the head file said
`enabled = false` literally. A change that deleted the gate's
`enabled = true` table, so that a gate off by default fell back to off,
was then hidden by --disable in CI. The skip now parses the head file
with the defaults applied and no command-line switch, and skips only
when that file still runs the gate. A read or parse error stops the
gate instead of being taken for a skip.

GATES.md said --config-override is left out of the diff by default; the
diff compares the configuration in force, so an override that loosens a
key is reported. The text now says so, and that only --disable alone is
left out.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…uns them

GATES.md named `cargo fetch --locked` and `npm install
--package-lock-only --ignore-scripts`; the presets run `cargo metadata
--locked --format-version 1` and `npm ci --ignore-scripts --dry-run`,
which does not write the lockfile. Each preset guards its manifest as
well as its lockfile. The four presets are the whole set; there is no
yarn, pnpm or pip preset.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
hook install never wired hook run --event post-tool, so the witnessed-runs path of [hooks.verified-stop] was unreachable from installed hooks and a stop could only be verified through run_at_stop. Each generated file now runs it after the agent's shell tool, by the names its recorded payloads carry: Bash (Claude Code, Codex), run_shell_command (Qwen Code), bash (Copilot CLI, repository and user-level files), run_command (agy), bash and shell (OpenCode plugin, which keeps 1.x arguments from tool.execute.before by callID). The witness never blocks, in either mode.

post-tool returns before reading the base ref or the tree when the working tree's discipline.toml does not mention verified-stop, and the witness returns when the base ref does not enable it. The hook-integrity reader recognises the new entry (JsonCommand::PostTool) and the OpenCode mode reader ignores it like the session-start command. The committed hook files are regenerated with hook install --observe --upgrade.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…gnore markers in code

(RUN: x), a bare RUN:, (RUN), [READ], `RUN`, **RUN** and -- RUN all satisfied a verification claim. A label is now RUN or READ, then :, an em or en dash, -- or a spaced -, then content naming a command, path or citation. Content is a placeholder when it is empty, a known placeholder word, a template slot such as <command>, a single filler word (done, ok), a first word of fewer than two letters or digits, or fewer than four letters, digits and path characters in all. A marker inside a code span that does not start with it (`echo RUN: x`) is code, not a label.

No existing test asserted one of the removed forms; all pass unchanged.

docs/benchmarks/claim-labels-precision.md now states that the study is in-sample, that recall is not measured, and that who Reader B was is not stated in the document or its history.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…de-constant direction and the live-session evidence note

The v0.21.1 preset row named six resolve-locked-* presets; src/guards/presets.rs defines four (cargo-, npm-, uv-, go-resolve-locked). The v0.20.0 equality-made-constant row said stricter while it capped a changed equality method made constant at warning, which v0.19.0 blocked. The live-session row now names the committed fixtures as its evidence and states that the acted-on step and the Qwen Code 0.25.2 Stop behaviour have no committed artifact.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…liverable report

The plugin generator's doc comment said an enforcing setup without tool.hook throws so the plugin fails closed; it only says so on stderr (console.error), which tests/test_agent_hooks.rs asserts (setupThrew == false). The comment now says what happens. withTimeout stops waiting for a command but cannot kill it, because Bun's $ returns a ShellPromise with no handle to the child process; the generated file and the doc comment now say so.

In the 2.x execute.after handler, a failed check whose call.result has neither a text part nor a string output dropped the report silently. In enforcing mode it is now said on stderr (console.error).

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
orieg added 5 commits October 15, 2026 20:09
The Package step fixed entry order, mtimes and the gzip header, but the archive still recorded the runner's build user and modes from the runner's umask, so the same commit packed on two runners could differ. Before archiving, directories are set 0755, files 0644 and the binary 0755; tar records owner and group 0 with no names (GNU tar on the Linux runners: --owner=0 --group=0 --numeric-owner; bsdtar on the macOS runners: --uid 0 --gid 0 --uname '' --gname ''). The listing after packing is verbose, so the job log shows the recorded owner and modes.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…nfinement and claim-labels

# Conflicts:
#	src/selftest.rs
The end-to-end test of the Claude Code post-tool entry put the binary's directory on PATH, a spawn tests/test_isolation.rs does not allow outside its list. It now checks that the entry is the missing-binary guard followed by hook run --agent claude-code --event post-tool, and runs those arguments through common::discipline_cmd.

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
…dgers

Adds unreleased Behaviour Changes rows for the hook, masking, research-gate, stub-bodies, suppression-delta, unsafe-budget, config-integrity and bounds fixes, and Default Changes rows for stub-bodies (changed equality method made constant) and citation-anchors (min_quote_chars).

Signed-off-by: Nicolas Brousse <nicolas@brousse.info>
@orieg
orieg enabled auto-merge (squash) October 16, 2026 04:09
@orieg
orieg merged commit d69f986 into main Oct 16, 2026
36 of 38 checks passed
@orieg
orieg deleted the fix/audit-v0-21-findings branch October 16, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Close the v0.19.0..v0.21.2 audit findings

1 participant