Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
2747191
Bound the harness audit and move it to claude-code-action v1.0.247
seathatflowsinourveins Oct 8, 2026
f4d2c31
Harden the bounded audit after an independent workflow audit
seathatflowsinourveins Oct 8, 2026
9015259
Set the harness audit's effort level explicitly
seathatflowsinourveins Oct 8, 2026
b97460e
Bound the harness audit's turns by assistant turns, not num_turns
seathatflowsinourveins Oct 9, 2026
9b361de
Fix the harness audit's bounds and declare every change it makes
seathatflowsinourveins Oct 9, 2026
26c3d87
Apply the pre-cue read's fix classes to the harness audit
seathatflowsinourveins Oct 9, 2026
41c54f1
Close the harness audit's partly fixed pre-cue findings
seathatflowsinourveins Oct 9, 2026
1be058d
Record that the deny rules bind Glob and Grep, measured against a con…
seathatflowsinourveins Oct 9, 2026
f194690
Bind repository debug settings in the guard, fix the cap notice, keep…
seathatflowsinourveins Oct 9, 2026
a285aa7
Refuse non-string tool entries, pin the client flags exactly and rais…
seathatflowsinourveins Oct 9, 2026
c5902b0
Bound the audit's cost at the budget times 1.10, publish within-bound…
seathatflowsinourveins Oct 9, 2026
50887d8
Fix the harness audit's J8 R5-R6 findings: exemption scope, budget-st…
seathatflowsinourveins Oct 9, 2026
dc61627
Pin the harness audit's action inputs and budget-stop floor that R7 c…
seathatflowsinourveins Oct 9, 2026
b41b969
Add the R8 source resolution for the harness audit's input-set and bu…
seathatflowsinourveins Oct 9, 2026
3c042de
Publish a budget-stopped audit's written text and keep instruction fi…
seathatflowsinourveins Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 227 additions & 13 deletions .github/workflows/harness-audit.yml

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions docs/decisions/2026-10-04-ci-least-privilege.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The existing write grants stay on their own jobs, and none runs on `pull_request
| --- | --- | --- |
| `catalog-freshness.yml:propose` | `contents: write`, `pull-requests: write` | schedule and dispatch only |
| `claude-pr-review.yml:review` (2026-10-08) | `id-token: write` | dispatch only, on `main`, by the owner; federation, not attestation (addendum below) |
| `harness-audit.yml:audit` (2026-10-08) | `id-token: write`, `issues: write` | schedule and dispatch only, on `main`; federation, not attestation ("Federation exemption (2026-10-08)") |
| `harness-audit.yml:audit` (2026-10-08) | `id-token: write` (`issues: write` removed 2026-10-08, see the addendum below) | schedule and dispatch only, on `main`; federation, not attestation ("Federation exemption (2026-10-08)") |
| `publish-catalog.yml:publish` | `id-token: write`, `attestations: write` | tag push and dispatch only; attests provenance |
| `publish-catalog.yml:release` | `contents: write` | tag push only |
| `saturation-tracking.yml:issue` | `issues: write` | schedule and dispatch only |
Expand Down Expand Up @@ -171,7 +171,7 @@ inventories.
| `claude-pr-review.yml` (added 2026-10-08) | `{}` | `review`: `contents: read`, `pull-requests: read`, `id-token: write` | | added | `id-token-write` exemption |
| `dependency-review.yml` | `{}` | `contents: read` | `none` | kept | |
| `hardware-profile-smoke.yml` | `{}` | `contents: read` on both jobs | `none` | kept | |
| `harness-audit.yml` (added 2026-10-08) | `{}` | `audit`: `contents: read`, `id-token: write`, `issues: write` | | added | `id-token-write` exemption |
| `harness-audit.yml` (added 2026-10-08) | `{}` | `audit`: `contents: read`, `id-token: write` (`issues: write` removed 2026-10-08, addendum below) | | added | `id-token-write` exemption |
| `native-foundation-e2e.yml` | `{}` | `contents: read` | `none` | added | |
| `native-offhost-app-state.yml` | unchanged (`contents: read`) | | | | hash-bound |
| `native-offhost-restore.yml` | unchanged (`contents: read`) | | | | hash-bound |
Expand Down Expand Up @@ -394,8 +394,9 @@ new rule yet.
workload identity federation: the action exchanges the job's GitHub OIDC token for a short-lived Claude API token, so
no Anthropic key is stored in this repository. That needs `id-token: write` on a job that attests nothing, which
`id-token-write` refuses, so `EXEMPTIONS` names the one job (`harness-audit.yml:audit`) with its reason and
`test_each_exemption_is_still_needed` drops the entry once it suppresses nothing. The job's two write grants,
`id-token: write` and `issues: write` (its one scorecard issue), are in the reviewed inventory.
`test_each_exemption_is_still_needed` drops the entry once it suppresses nothing. The job's write grant,
`id-token: write`, is in the reviewed inventory (it also held `issues: write` for its scorecard issue until the
bounded audit removed it on 2026-10-08; addendum below).

The federation rule accepts workflows on this repository's `main` and never pull requests: it matches the OIDC subject
of a run on `main`, and a pull request run's subject ends in `:pull_request` instead (GitHub's OpenID Connect reference,
Expand Down Expand Up @@ -438,6 +439,16 @@ requires, it has its own entry in the reviewed inventory (`id-token: write`, its
requires `refs/heads/main` and the owner
([2026-10-09-claude-actions-pr-toolkit-review.md](2026-10-09-claude-actions-pr-toolkit-review.md)).

### Addendum (2026-10-08): the audit job gives up `issues: write`

The two tables above listed `harness-audit.yml:audit` with `id-token: write` and `issues: write`, as it was added;
they now show the grant as removed. The
bounded audit ([2026-10-08-claude-actions-harness-audit-bounds.md](2026-10-08-claude-actions-harness-audit-bounds.md))
removes the model's `gh issue create` tool and the `issues: write` grant: the report goes to the job summary from a
model-free step. The job's only write grant is now `id-token: write`, the reviewed inventory in
`tests/test_workflow_policy.py` says so, and this exemption is unchanged. The action pin moves to v1.0.247
(`2dca132ff0e0c4094ce6048b422c6915a071210b`), whose federation inputs are the four named above.

## Alternatives considered

- **Keep workflow-level `contents: read`.** It already met OpenSSF Scorecard's Token-Permissions top score (read-only
Expand Down
712 changes: 712 additions & 0 deletions docs/decisions/2026-10-08-claude-actions-harness-audit-bounds.md

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions docs/github-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@ The Actions allow-list and harness description below were checked against live s
request (`test_pull_requests_write_is_granted_only_to_the_propose_job`,
`test_no_workflow_reviews_or_approves_a_pull_request`).
- **Harness audit.** The weekly and manually dispatched [`harness-audit.yml`](../.github/workflows/harness-audit.yml)
runs only on `main`, using `anthropics/claude-code-action` v1.0.245 at
[`6fed3ca145920b639991cb756090506e1bcaf515`](https://github.com/anthropics/claude-code-action/blob/6fed3ca145920b639991cb756090506e1bcaf515/action.yml).
Its job grants `contents: read`, `id-token: write` and `issues: write` over an empty workflow permission default.
runs only in `seathatflowsinourveins/native-agent-stack` and only on `main`, using `anthropics/claude-code-action`
v1.0.247 at
[`2dca132ff0e0c4094ce6048b422c6915a071210b`](https://github.com/anthropics/claude-code-action/blob/2dca132ff0e0c4094ce6048b422c6915a071210b/action.yml),
only while the repository variable `CLAUDE_HARNESS_AUDIT_ENABLED` is `true`, only on a run's first attempt, and,
when dispatched by hand, only when the repository owner is both `github.actor` and `github.triggering_actor`.
Its job grants `contents: read` and `id-token: write` over an empty workflow permission default.
Anthropic workload identity federation exchanges GitHub's OIDC token using the four configured repository variables;
the workflow fetches a pinned, SHA-256-verified audit prompt, asks for a project-scoped read-only audit and one
scorecard issue, and allows only `gh issue create` as a Bash tool. Hosted acceptance remains open after
the workflow fetches a pinned, SHA-256-verified audit prompt and asks for a project-scoped read-only audit. Claude
has Read, Glob and Grep only, at most 20 assistant turns (checked after the run) and a $5 client budget; a
model-free step copies the report to the
job summary and keeps the run's token and cost numbers
([decision](decisions/2026-10-08-claude-actions-harness-audit-bounds.md)). Hosted acceptance remains open after
[run 37739403956 failed](https://github.com/seathatflowsinourveins/native-agent-stack/actions/runs/37739403956).
The owner checks the native [Claude Console federation history](https://platform.claude.com/settings/workload-identity-federation?tab=history)
for the reason and corrects the relevant rule or claim; the Console denial reason is not established by the retained
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
{
"schema_version": 1,
"what": "Does harness-audit.yml's --settings deny list bind Glob and Grep as well as Read? A probe run with the workflow's fence flags and --settings verbatim, and a control run with the deny rules removed, on the installed Claude Code in a scratch git repository holding marker strings in .git, a nested repository's .git and .env. Tool calls and results are taken from each run's stream, never from the model's account; the streams stay outside the repository and are named by their sha256.",
"recorded_utc": "2026-10-09T04:25:24Z",
"scratch_repository": {
"markers": {
".git/PROBE_MARKER": "J8-PROBE-SECRET-GIT",
"sub/.git/PROBE_MARKER": "J8-PROBE-SECRET-NESTED",
".env": "J8-PROBE-SECRET-ENV"
}
},
"runs": {
"probe": {
"started_utc": "2026-10-09T03:14:18Z",
"exit_code": 0,
"client": "2.1.295",
"model": "claude-haiku-5-5",
"auth": "subscription (apiKeySource none)",
"session_tools": [
"Glob",
"Grep",
"Read"
],
"result_subtype": "success",
"argv": [
"claude",
"-p",
"<the probe prompt>",
"--model",
"claude-haiku-5-5",
"--max-turns",
"12",
"--tools",
"Read,Glob,Grep",
"--allowedTools",
"Read,Glob,Grep",
"--restricted",
"--permission-prompts",
"none",
"--setting-sources",
"user",
"--strict-mcp-config",
"--settings",
"{\"disableAllHooks\":true,\"permissions\":{\"blockReadsOutsideWorkingDirectories\":true,\"deny\":[\"Read(./.git/**)\",\"Read(./**/.git/**)\",\"Read(./**/.env)\",\"Read(./**/.env.*)\",\"Read(./**/*.pem)\",\"Read(./**/*.key)\"]}}",
"--no-session-persistence",
"--output-format",
"stream-json",
"--verbose"
],
"settings": "{\"disableAllHooks\":true,\"permissions\":{\"blockReadsOutsideWorkingDirectories\":true,\"deny\":[\"Read(./.git/**)\",\"Read(./**/.git/**)\",\"Read(./**/.env)\",\"Read(./**/.env.*)\",\"Read(./**/*.pem)\",\"Read(./**/*.key)\"]}}",
"stream_sha256": "4a009a8cd1987c6b69f5d7452a3436a25179783624b2e3a24c0ddc1e24d98c7e",
"stream_bytes": 24758,
"steps": [
{
"tool": "Glob",
"input": {
"pattern": ".git/**"
},
"outcome": "returned nothing",
"markers_returned": []
},
{
"tool": "Glob",
"input": {
"pattern": "**/PROBE_MARKER"
},
"outcome": "returned nothing",
"markers_returned": []
},
{
"tool": "Grep",
"input": {
"pattern": "J8-PROBE-SECRET",
"path": ".",
"output_mode": "content"
},
"outcome": "returned nothing",
"markers_returned": []
},
{
"tool": "Grep",
"input": {
"pattern": "J8-PROBE-SECRET",
"path": ".git",
"output_mode": "content"
},
"outcome": "refused",
"markers_returned": []
},
{
"tool": "Grep",
"input": {
"pattern": "J8-PROBE-SECRET",
"path": "sub/.git",
"output_mode": "content"
},
"outcome": "refused",
"markers_returned": []
},
{
"tool": "Read",
"input": {
"file_path": ".git/PROBE_MARKER"
},
"outcome": "refused",
"markers_returned": []
},
{
"tool": "Read",
"input": {
"file_path": ".env"
},
"outcome": "refused",
"markers_returned": []
}
]
},
"control": {
"started_utc": "2026-10-09T03:40:21Z",
"exit_code": 0,
"client": "2.1.295",
"model": "claude-haiku-5-5",
"auth": "subscription (apiKeySource none)",
"session_tools": [
"Glob",
"Grep",
"Read"
],
"result_subtype": "success",
"argv": [
"claude",
"-p",
"<the probe prompt>",
"--model",
"claude-haiku-5-5",
"--max-turns",
"12",
"--tools",
"Read,Glob,Grep",
"--allowedTools",
"Read,Glob,Grep",
"--restricted",
"--permission-prompts",
"none",
"--setting-sources",
"user",
"--strict-mcp-config",
"--settings",
"{\"disableAllHooks\":true,\"permissions\":{\"blockReadsOutsideWorkingDirectories\":true}}",
"--no-session-persistence",
"--output-format",
"stream-json",
"--verbose"
],
"settings": "{\"disableAllHooks\":true,\"permissions\":{\"blockReadsOutsideWorkingDirectories\":true}}",
"stream_sha256": "c9144c4c04683e1aa3116770b155e786f94a99b2ac50dd174161bd82db9cd6aa",
"stream_bytes": 26333,
"steps": [
{
"tool": "Glob",
"input": {
"pattern": ".git/**"
},
"outcome": "returned files",
"markers_returned": []
},
{
"tool": "Glob",
"input": {
"pattern": "**/PROBE_MARKER"
},
"outcome": "returned files",
"markers_returned": []
},
{
"tool": "Grep",
"input": {
"pattern": "J8-PROBE-SECRET",
"path": ".",
"output_mode": "content"
},
"outcome": "returned markers",
"markers_returned": [
"env"
]
},
{
"tool": "Grep",
"input": {
"pattern": "J8-PROBE-SECRET",
"path": ".git",
"output_mode": "content"
},
"outcome": "returned markers",
"markers_returned": [
"git"
]
},
{
"tool": "Grep",
"input": {
"pattern": "J8-PROBE-SECRET",
"path": "sub/.git",
"output_mode": "content"
},
"outcome": "returned markers",
"markers_returned": [
"nested"
]
},
{
"tool": "Read",
"input": {
"file_path": ".git/PROBE_MARKER"
},
"outcome": "returned markers",
"markers_returned": [
"git"
]
},
{
"tool": "Read",
"input": {
"file_path": ".env"
},
"outcome": "returned markers",
"markers_returned": [
"env"
]
}
]
}
},
"finding": "With the deny rules every call returned nothing or was refused; without them the same calls listed .git files, found both markers, returned the .env marker through Grep over '.', and read each marker. So the empty Glob and Grep results come from the deny rules. Grep over '.' skips .git in both runs.",
"limits": [
"one model (Haiku 5.5) and one prompt per arm, on the subscription, not through the action or on a runner",
"--add-dir and --effort were not passed in either run; --setting-sources user, --restricted and the tools were"
]
}
32 changes: 26 additions & 6 deletions manifests/evidence.json
Original file line number Diff line number Diff line change
Expand Up @@ -5049,6 +5049,11 @@
"sha256": "17264678834e8c4ef47f0bc0820a980310eb71676db5f07500358119b53b3753",
"bytes": 5951
},
{
"path": ".github/workflows/harness-audit.yml",
"sha256": "22c470acd8d8896d8fc215697bde0246fe152cb308f4fa53a510b64b3799bb87",
"bytes": 18634
},
{
"path": ".github/workflows/native-foundation-e2e.yml",
"sha256": "ba868797728981c49effa4094a39c70466073c0a98d78fd893c296e71b2c09d7",
Expand Down Expand Up @@ -18027,8 +18032,8 @@
},
{
"path": "docs/decisions/2026-10-04-ci-least-privilege.md",
"sha256": "f97025072022743b2b3f10a38efc45017849c7b3b644c8919a86eca542e94174",
"bytes": 42252
"sha256": "69f7fcd2c2f91412dc478f8d10503820a186efba9788f09f723d3212695cf3d0",
"bytes": 43157
},
{
"path": "docs/decisions/2026-10-04-claude-template-holds-out-token-lane-carriers.md",
Expand Down Expand Up @@ -18310,6 +18315,11 @@
"sha256": "ccedf4766467974b092306934ace409193f71e2918fce7ac8449b054d6461049",
"bytes": 6955
},
{
"path": "docs/decisions/2026-10-08-claude-actions-harness-audit-bounds.md",
"sha256": "e74fced9eaa05161cdb335b070b880219d59625e05e3f7d5e6863e3b4f3839ac",
"bytes": 63427
},
{
"path": "docs/decisions/2026-10-08-claude-actions-pr-review.md",
"sha256": "ae0a32b4875d7ddae697cbb9c2d2f18fb5accaed69863e3511e3fc6049cc3bed",
Expand Down Expand Up @@ -18527,8 +18537,8 @@
},
{
"path": "docs/github-automation.md",
"sha256": "ccc7c8b9c951a39e537f42819a5c29ece64a8db5c6d4caf60d70e9dc945d61c6",
"bytes": 103436
"sha256": "28a97e3dea87e15a141bb297e4654fc235bc7445ba54941a23e19de90eaf75c8",
"bytes": 103924
},
{
"path": "docs/grand-catalog-handbook.md",
Expand Down Expand Up @@ -19770,6 +19780,11 @@
"sha256": "5c1e13b427dd9c8818721a1788eab19d55ef0ceef269d5eb644c25557760238b",
"bytes": 39479
},
{
"path": "evidence/artifacts/claude-actions-fence-smoke-20261008/deny-probe-receipt.json",
"sha256": "7b6fcee51e75d84623ae80474f4864d181358db8f101b66ff78b67f07a264e27",
"bytes": 6272
},
{
"path": "evidence/artifacts/claude-actions-fence-smoke-20261008/local-parity-receipt.json",
"sha256": "ba498b82b6b4d2410879f23112da5d1aeaf5dabe008e7207a56e867dc48b1477",
Expand Down Expand Up @@ -59785,6 +59800,11 @@
"sha256": "73d9b3ccf9ef348d1cd74494a7e7fdc169666b1b748d7310a78a2913f5fd55dd",
"bytes": 22660
},
{
"path": "tests/test_claude_harness_audit_bounds.py",
"sha256": "2119d4e93b000d820d0018364cd2daace48b2b5cc9f61e6630b41e88e550b41c",
"bytes": 32715
},
{
"path": "tests/test_claude_pr_review_workflow.py",
"sha256": "cfaa2b171fcffdd1c31ce65269482efe15a5d6b9a515330f81aa314bd8274759",
Expand Down Expand Up @@ -60692,8 +60712,8 @@
},
{
"path": "tests/test_workflow_policy.py",
"sha256": "598f6d8ea8d5b7224581396b35b998a9cec7440f2e7b849acf97bde4a9c5c36f",
"bytes": 84974
"sha256": "2c132e9fee2c59d15bac9b42d28c6bb9b53ecfebcf331066199b9178fecf065f",
"bytes": 85103
},
{
"path": "tests/test_workflow_security.py",
Expand Down
Loading
Loading