Skip to content

[bench] security: stop cross-project ledger leak, retire contaminated chain, add disclosure policy#13

Merged
dburks-svg merged 4 commits into
mainfrom
bench/security-policy-and-hardening
Jul 24, 2026
Merged

[bench] security: stop cross-project ledger leak, retire contaminated chain, add disclosure policy#13
dburks-svg merged 4 commits into
mainfrom
bench/security-policy-and-hardening

Conversation

@dburks-svg

@dburks-svg dburks-svg commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

Started as "add a SECURITY.md." Surfaced a live leak: this public repo was
publishing source code from unrelated private projects. Four commits: stop the
leak, minimize what the ledger records, retire the contaminated chain under a
bounded constitutional amendment, and add a regression guard.

1. The leak

Bench's hook is registered globally in ~/.claude/settings.json, so it governs
every project on the machine. But ledger/chain.py pinned the ledger to the
Bench install directory, so every governed edit anywhere appended its full
diff into this public repo's chain.

Entries Foreign Size Diff bodies
Already public 490 264 4.4 MB 705
Uncommitted 1,882 1,715 17.8 MB 2,313

Exposed: another project's src/, an Electron app, a Next.js billing app,
.claude/plans/, project memory notes, the global settings.json. Scanned for
12 credential classes; every hit was fixture data, so nothing needs rotating.

Fix: resolve_ledger_path() routes by the project being governed.
BENCH_LEDGER_PATH wins; else a cwd inside the Bench repo uses Bench's own
ledger; else <project>/.bench/bench-ledger.json. load_ledger,
verify_chain, and the viewer all default to the same resolver, so the auditor
can never inspect a different file than the writer appends to.

2. Redaction of out-of-project bodies

Routing fixes other projects' sessions. A file edited from a Bench session
still lands here, so out-of-project diff bodies are now stripped before hashing:
content, old_string, new_string, edits, formatted_diff are dropped and
the entry is marked redacted. Path, tool, and verdict survive, so the audit
trail still shows the file was governed and how it was ruled on.

Classification and routing share one _project_root() anchor. They previously
disagreed: editing utils/api.py while sitting in tests/ would have been
judged external and stripped evidence from Bench's own record.

Residual, stated plainly: judge prose may still quote a few lines of the change
it reasons about. This removes the systematic copy of file bodies, not every
possible quotation.

3. C-008 amended, chain retired (constitution v2)

Bench vetoed its own chain reset under C-008, correctly: removing a ledger
is deletion. Its proposed alternatives (keep the contaminated chain in-repo, or
append to it) both leave 1,715 foreign diffs in a public repo. C-008 and the
privacy requirement genuinely conflicted, so the law was amended rather than
circumvented.

Retirement is now permitted with one trigger (the chain contains content that
must not be published) and four mandatory elements: a human decision that is
never agent-initiated, an archive passing verify_chain before the original
moves, an anchor recording tip hash, genesis hash, entry count, timestamps,
verbatim archive path and reason, and indefinite retention. Editing, reordering,
or removing an individual entry remains forbidden with no exception.

A first draft of the amendment was itself vetoed under C-007 for being
open-ended and unenforceable. The landed version names the authority, defines
the verification step, and gives auditors a concrete check.

The retirement then ran:

retired entries : 2471 (2026-04-22 to 2026-07-24)
tip hash        : 2176516fec72f66da131096438cd318d8453409f87dec30107a312beef7a4c8d
anchor entry    : 7a06ce4f-aa12-4129-ada3-d200ece60cdb

The auditor check C-008 prescribes was run against the result: the archive
verifies valid at 2471 entries, and its tip hash, genesis hash, and entry count
all match the anchor. Auditable, not asserted.

ANCHOR verdicts are plumbed through stats and the CLI so retirement markers
never skew the pass rate. Dry-run confirmed verify, stats, and the HTML viewer
handle the new entry shape without erroring.

4. Regression guard

tests/test_ledger_hygiene.py asserts no ledger entry carries a diff body under
a non-relative path. Verified non-vacuous: run against the retired chain it
reports 349 violations, so it would have caught this in April.

Deliberately stricter than the runtime redactor: it flags any absolute path,
including one pointing inside the repo, since that means either foreign source
or a path-normalization regression. Absoluteness is matched by regex, not
os.path.isabs, because CI runs on Linux where isabs(r"C:\Users\...") is
False and a Windows-shaped path is exactly what the guard exists to catch.

5. SECURITY.md and CI hardening

Policy written against Bench's real threat model. In scope: governance
bypass, fail-open regressions, ledger forgery that still passes cli verify,
prompt injection through governed content, credential exposure. Out of scope:
model judgment quality (a disagreeable Oracle verdict is a constitution issue,
not a CVE). States plainly that the ledger is tamper-evident, not tamper-proof.

  • permissions: contents: read at workflow level
  • actions/checkout v4 to v7, actions/setup-python v5 to v7 (aliases verified
    via the refs API; CI green on 3.11, 3.12, 3.13)
  • dependabot.yml for pip and github-actions; alerts and security fixes enabled

Do not enable auto-merge on Dependabot PRs. That routes dependency changes
around the pipeline, which C-007 forbids.

Governance record

Bench governed every change here and rejected four of them:

  1. VETO (C-004) on a bare Any annotation. Complied.
  2. VETO (C-007) on the first constitutional amendment as unenforceable.
    Rewritten with a named authority and an auditor-executable check.
  3. VETO (C-008) on the chain reset itself, before the amendment existed.
  4. VETO (C-008(a)) on the retirement script, because the human decision was
    a narrative string authored by an agent. The gate is now structural, and it
    refused two non-interactive invocations from me before the owner ran it.

Plus one fail-closed VETO when a Defender stage errored, and Oracle catches
on a missing Path import, an unverified resolve_ledger_path import, and an
irreversible C-008 hazard in a test that would have written synthetic entries
into the real chain.

Testing also caught what review did not: the first isatty() human gate silently
failed under Git Bash, where msys reports a TTY even with stdin redirected. The
EOFError path is what actually holds.

Not fixed

tests/test_hook.py:371 has a pre-existing F841 (unused original_cwd), left
alone as unrelated. Ruff is not in CI.

Already-public history is unchanged by deliberate decision: 56 unique cloners
pulled this repo in the preceding 14 days, so a rewrite cannot un-distribute the
264 foreign entries, and severing the chain would cost more than it recovers.

dburks-svg and others added 2 commits July 24, 2026 10:52
Adds SECURITY.md at the repo root so GitHub surfaces a disclosure route
instead of pointing reporters at public issues.

The policy is scoped to what a Bench vulnerability actually is: governance
bypass, fail-open regressions, ledger forgery that still passes
`python -m cli verify`, prompt injection through governed content,
constitution snapshot violations, and credential exposure. Model judgment
quality is explicitly out of scope; a disagreeable Oracle verdict is a
constitution issue, not a CVE.

Every runtime claim in the document was verified against code rather than
docs: MAX_DIFF_LINES=300 (utils/diff.py:29), the BENCH_SUBPROCESS
reentrancy guard as the sole fail-open path (hooks/pre-tool-use.py:290),
and pipeline_error VETO on judge failure (pipeline/runner.py).

CI hardening in the same pass:
- workflow-level `permissions: contents: read` (least privilege; the job
  only reads the repo)
- actions/checkout v4 -> v7, actions/setup-python v5 -> v7 (both major
  aliases confirmed to exist before pinning)
- dependabot.yml covering pip and github-actions

Dependabot PRs must not be set to auto-merge: that would route dependency
changes around the governance pipeline, which C-007 forbids.

The ledger is deliberately NOT included in this commit. It has accrued
entries from unrelated projects on this machine and needs a separate
decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QFU7kbe53G7BPUZHprb9G
Bench's PreToolUse hook is registered globally in ~/.claude/settings.json,
so it governs every project on this machine. But ledger/chain.py pinned the
ledger path to the Bench install directory, so every governed edit anywhere
appended its full diff to Bench's own ledger. Since that ledger is committed
to a public repository, unrelated projects' source was being published: 264
foreign entries already public, 1715 more staged in the working tree.

resolve_ledger_path() now routes by the project being governed:

  BENCH_LEDGER_PATH   -> that path (explicit central ledger)
  cwd inside Bench    -> ledger/bench-ledger.json (self-governance, unchanged)
  anything else       -> <project>/.bench/bench-ledger.json

Readers move with the writer. load_ledger, verify_chain, and the HTML viewer
all default to the same resolver, so the auditor can never inspect a
different file than the writer appends to. verify.py keeps its independence
where it matters: it still recomputes every hash itself and never calls
load_ledger. Agreeing on which file to audit is a precondition for auditing
it, not a coupling.

ledger-meta.json is written alongside whichever ledger is selected, so each
chain carries its own anchor and verifies standalone.

cli verify now prints the ledger it read, via _display_path, which collapses
to a CWD-relative form so operators do not paste their home directory into
bug reports.

Tests cover all four resolution branches, Bench-subdirectory resolution, and
reader/writer parity. The parity test asserts the resolved target is inside
its fixture before calling append_entry: if routing regressed, that append
would land in the real append-only chain and could not be removed under
C-008. Verified: 2410 entries before and after the run, zero synthetic.

Bench governance on this change: every edit was challenged, defended, and
ruled on. The Oracle caught a missing Path import in cli/commands.py, an
unverified resolve_ledger_path import in utils/viewer.py, the irreversible
C-008 hazard in the parity test, and required that the verifier move with
the writer rather than landing the write path alone. One edit was
fail-closed VETOed when the Defender stage errored, and was retried.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QFU7kbe53G7BPUZHprb9G
@dburks-svg dburks-svg changed the title [bench] security: add disclosure policy and harden CI supply chain [bench] security: scope ledger per project, add disclosure policy, harden CI Jul 24, 2026
dburks-svg and others added 2 commits July 24, 2026 13:06
…tirement

Two changes, both driven by the cross-project contamination fix.

1. Out-of-project diff bodies are stripped before hashing.

A globally registered hook governs files belonging to other projects. Those
changes are still adjudicated in full, but the recorded evidence now keeps
only path, tool and verdict: content, old_string, new_string, edits and
formatted_diff are dropped, and the entry is marked redacted. A published
ledger therefore cannot become a mirror of another project's source.

Classification and ledger routing now share one anchor, _project_root().
They previously disagreed: routing treated any cwd under the Bench repo as
self-governance, while classification compared against the raw cwd. Editing
utils/api.py while sitting in tests/ would have been judged external and
stripped evidence out of Bench's own record. The Oracle caught this.

Residual, stated plainly: challenger/defender/oracle prose may still quote a
few lines of the change it reasons about. This removes the systematic copy of
file bodies, not every possible quotation.

2. C-008 gains a bounded chain-retirement clause (constitution v2).

Bench vetoed its own chain reset under C-008, correctly: removing a ledger is
deletion, and the rule admitted no exceptions. But the alternatives it
proposed (keep the contaminated chain in-repo, or append to it) both leave
1,715 foreign diffs in a public repository. C-008 and the privacy requirement
genuinely conflicted, so the law was amended rather than circumvented.

Retirement is now permitted, with a single trigger (the chain contains content
that must not be published) and four mandatory elements: a human decision that
is never agent-initiated, an archive that passes verify_chain BEFORE the
original moves, an anchor entry recording the retired chain's tip hash,
genesis hash, entry count, timestamps, verbatim archive path and reason, and
indefinite retention. Editing, reordering or removing an individual entry
remains a violation with no exception.

A first draft of this amendment was itself vetoed under C-007 for being
open-ended and unenforceable. The version here names the authority, defines
the verification step, and gives auditors a concrete check.

ANCHOR verdicts are plumbed through stats and the CLI so retirement markers
are counted separately from adjudicated changes and cannot skew the pass rate.
Dry-run confirmed verify, stats and the HTML viewer all handle the new entry
shape without erroring.

Tests: 126 pass across chain, stats, commands, verify, viewer, constitution.
Includes coverage for every resolution branch, external classification from a
subdirectory, redaction of bodies, and anchor accounting.

Note: tests/test_hook.py:371 has a pre-existing F841 (unused original_cwd),
untouched here since it is unrelated to this change. Ruff is not in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QFU7kbe53G7BPUZHprb9G
The chain that ran from 2026-04-22 to 2026-07-24 is retired. It accumulated
governance receipts from unrelated projects on this machine, because a
globally registered hook wrote every project's diffs into Bench's own ledger.
Publishing it would disclose third-party source.

Retirement was performed under C-008 as amended in constitution version 2,
and was initiated by the repository owner at an interactive prompt. The
script refused two non-interactive invocations before that, which is C-008(a)
working as written.

  retired entries : 2471 (2026-04-22 to 2026-07-24)
  tip hash        : 2176516fec72f66da131096438cd318d8453409f87dec30107a312beef7a4c8d
  archive         : C:\Users\mstar\.bench-archive\bench-ledger-2026-07-24.json
  anchor entry    : 7a06ce4f-aa12-4129-ada3-d200ece60cdb

No entry was edited, reordered, or removed. The chain was archived intact,
the archive was verified with verify_chain BEFORE the original was moved, and
the successor's anchor entry records the predecessor's tip hash, genesis hash,
entry count, timestamps, verbatim archive path, and reason.

The auditor check C-008 prescribes was run against the result: the archive
verifies valid at 2471 entries, and its tip hash, genesis hash, and entry
count all match the values recorded in the anchor. Retirement is auditable,
not asserted.

tests/test_ledger_hygiene.py is the regression guard. It asserts no ledger
entry carries a diff body under a non-relative path. Verified non-vacuous:
run against the retired chain it reports 349 violations, so it would have
caught this contamination in April.

The guard is deliberately stricter than the runtime redactor. It flags any
absolute path, including one pointing inside the repo, because that means
either foreign source or a path-normalization regression. The retired chain
contained both kinds. Absoluteness is matched by regex rather than
os.path.isabs, since CI runs on Linux where isabs of a Windows path is False
and a Windows-shaped path is exactly what the guard exists to catch.

This commit resumes the practice of shipping the ledger as source. The
successor chain is 7 entries: the anchor plus the governed writes that
produced this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QFU7kbe53G7BPUZHprb9G
@dburks-svg dburks-svg changed the title [bench] security: scope ledger per project, add disclosure policy, harden CI [bench] security: stop cross-project ledger leak, retire contaminated chain, add disclosure policy Jul 24, 2026
@dburks-svg
dburks-svg merged commit 8880a65 into main Jul 24, 2026
6 checks passed
dburks-svg added a commit that referenced this pull request Jul 24, 2026
Ten receipts generated after the final commit on the PR branch: scratchpad
and memory writes during session wrap-up. All ten are out-of-project files,
so every one is recorded with path and verdict but a redacted body. This is
the redaction landed in PR #13 working in production, not a special case.

Chain valid at 19 entries from the successor genesis 4e98fb41.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QFU7kbe53G7BPUZHprb9G
@dburks-svg
dburks-svg deleted the bench/security-policy-and-hardening branch July 24, 2026 20:41
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.

1 participant