Skip to content

Hardening: audit, accuracy fixes, isolation and credential fixes, tests, CI - #1

Merged
chief-builder merged 21 commits into
mainfrom
hardening/2026-10-02
Oct 3, 2026
Merged

chief-builder merged 21 commits into
mainfrom
hardening/2026-10-02

Conversation

@chief-builder

@chief-builder chief-builder commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Hardening pass: audit, accuracy fixes, isolation and credential fixes, tests, CI, and repo hygiene. The full audit with evidence is in AUDIT.md (§2 lists every claim; the addendum covers what came up while implementing).

Read first: two findings change published conclusions.

  1. GitHub tier1_issue_triage is not a valid comparison. The n5 agent token had no Issues read access, so every issue call returned 403 in both arms. MCP's 0/5 was a permissions failure, not "fan-out". The skill arm's 5/5 came entirely from escalated credentials: the controller token in 3 trials and the author's own gh login in 2.
  2. The env scrub never worked, including after 90d0dd7, because execa merged the parent environment back into the child. Fixed here. If those tokens are still live, rotate them.

1. Summary of changes

Security and isolation (d999386, a6ed4ec)

  • Every arm passes a positive claude --tools list. --allowed-tools never restricted anything under bypassPermissions; agents had Read, Glob, Grep, Edit and more in every arm.
  • File tools are denied under ~ and the repo through --settings.
  • The GitHub skill arm's Bash runs in Claude Code's OS sandbox (allowUnsandboxedCommands: false, denyRead: ["~/"]). This blocks the keychain path to the developer's gh login. Verified live: gh auth token and security find-generic-password are both BLOCKED.
  • The env scrub uses extendEnv: false and removes every GH_*/GITHUB_* variable. GH_CONFIG_DIR points at an empty directory per trial.
  • Transcripts and results are redacted before they are written: home paths become ~, and file-tool results for paths outside the trial directory are replaced with a marker.

Measurement correctness

  • Timed-out trials recorded 0 tokens and 0 ms. They now carry lower-bound estimates summed from per-message usage, flagged tokensEstimated, and the timeout as wall clock.
  • The classifier is now an allow-list derived from the same per-arm tool list. The Playwright helper regex no longer flags words inside URLs or JS. Both classifiers now treat base64 as a helper.
  • All committed results were recomputed (9450f9e). 4 verdicts flipped to invalid surface, all on failed trials; no pass/fail value changed.

Data hygiene (9450f9e)

  • 306 artifact files were redacted. Committed transcripts contained the author's email (from ~/.gitconfig), fragments of ~/.claude.json and shell rc files, gh config, and home-directory listings naming private projects.
  • No credentials were present: gitleaks reports 0 findings across all 36 prior commits.
  • This does not remove the data from git history (see open items).

Design

  • New config.ts holds the model, timeout and tool lists, plus a zod-validated GitHub config; .env is now actually loaded.
  • Cleanup and temp-dir removal run in finally, so a failed trial can't leak a sandbox repo.
  • CLI argument validation (--trials abc and --run ../x are rejected); no stack traces for unknown experiments.
  • New redact-artifacts command; a small logger with JSON output and token redaction.
  • Dead code removed: mkSeed, hasShellRedirection, LoadedExperiment, unused agentEnv, readJsonIfExists, and the guessed MCP tool-name lists.
  • verify-arms now starts each arm exactly like a trial and prints the real tool list from the init event. It also gained the documented --arm option.

Tests (dd15e74)

  • 80 Vitest tests, runnable with no network or Claude calls.
  • Unit tests cover the classifiers, shell parsing, metrics (including timeouts), env scrub and token routing (positive and negative), trial settings, config validation, log redaction, fixture-server traversal and size limits, redaction, and seeds.
  • An integration test runs runTrial end to end with a fake claude over the real tier1_scrape fixture: scoring, redaction, cleanup, and the timeout path. CLI tests cover argument validation and preflight.

Currency (ffcff61)

  • Node 24 LTS is pinned (.nvmrc, engines) and pnpm 12.8.1 (packageManager).
  • Upgraded: execa 10, commander 15, zod 4.6, tsx 4.23 (clears the only pnpm audit finding, a low-severity esbuild advisory), and @types/node 24.
  • Held back:
    • TypeScript 6.0.3: typescript-eslint 8.71 refuses TS 7.0.
    • Experiment subjects stay pinned: @playwright/cli 0.1.13 (latest 0.1.22), @playwright/mcp 0.0.75 (0.0.83), github-mcp-server v1.0.4 by digest (v1.13.0), model claude-sonnet-4-6. Upgrading them changes what is measured, so they should move with a new run.
  • MCP spec: the harness doesn't implement MCP; Claude Code is the client. The current revision is 2026-07-28. This is documented under Limitations.

Docs (f11b5c4, 4916b83, 182f56f)

  • README: rewritten to the requested structure (Mermaid architecture, quickstart, configuration table, status and limitations, license, CI badge).
  • CLAUDE.md: updated to match the code.
  • Findings: tables regenerated from the recomputed data; your narratives are kept and corrected in place, each with a dated correction note.
  • Docs site:
    • The v2 pages were corrected, and the overview carries a "Corrections (October 2026)" note.
    • The v1 pages were removed (per your instruction). index.html still redirects to the v2 overview, so Pages keeps working.
    • Two images that illustrated disproven claims were removed: playwright-cycle-cost.png and surface-escape.png.

CI/CD and hygiene (2066769, e8b9ccd)

  • CI: frozen install, lint, Prettier check, typecheck, tests with coverage (shown in the job summary), CLI smoke, and a lychee link check.
  • CodeQL: JS/TS and Actions.
  • Dependabot: npm and Actions.
  • Workflow hardening: top-level contents: read, SHA-pinned actions, concurrency groups, per-job timeouts.
  • Repo files: MIT LICENSE, NOTICE plus third_party/ for the Apache-2.0 skill adapted from @playwright/cli, SECURITY.md, CONTRIBUTING.md, CHANGELOG.md (1.1.0), .editorconfig, issue/PR templates, .env.example, and a broader .gitignore.
  • Fake test tokens: labelled, and test/ is excluded from GitHub secret scanning and gitleaks.

2. Claims table

Status is the final state after this PR. "Fixed" means the claim was wrong and has been corrected (or the code now makes it true); "Verified" means it was already right. Docs-site claims (182 rows) are in AUDIT.md §2c; the rows below include every README claim plus the docs-site items that changed conclusions.

# Claim (original) Audit Final Evidence
R1–R3 Three arms; @playwright/mcp@0.0.75; digest-pinned github-mcp-server Verified Verified experiment.ts, .mcp.*.json; digest = v1.0.4 (GHCR manifest match)
R4 Records tokens, turns, time, success, validity Verified with defect Fixed Timeouts recorded 0/0 → estimates (metrics.ts, test metrics.test.ts)
R5, R7 Paired seed shared by arms; answers off disk Verified Verified trialState.test.ts; fixtureServer.ts
R6 Paired seed "via FNV-1a" Wrong Fixed (reworded: SHA-256) trialState.ts:mkPairedSeed
R8 "Positive --allowed-tools list"; per-arm "Allowed" tables; skill arm without Read/Glob/Grep Wrong Fixed (code now enforces it with --tools) n5 init events listed Read/Glob/Grep/Edit… in all arms; verify-arms on 2.1.287 now shows exactly the configured lists
R9 --setting-sources keeps ~/.claude skills out Partly wrong Fixed (reworded) Built-in skills still load; only the skill arm has the Skill tool
R10 Five tools always blocked Verified Verified config.ts, --disallowed-tools
R11, R12 Model claude-sonnet-4-6; 240 s timeout Verified Verified config.ts
R13 GitHub tier1.ts "(3 tasks)" Wrong Fixed 4 Tier 1 + 4 Tier 2 tasks
R15 verify-arms verifies surfaces Wrong Fixed (re-implemented) Runs in a trial tempdir, reads init.tools, fails on extras
R16 23 Playwright MCP tools Verified Verified verify-arms output below
R17, R18 Login PNG ≥ 1 KB; repo naming Verified Verified tier1.ts, trialState.test.ts
R19, R20 Schema lists numTurns; reason on toolCalls Wrong Fixed (README points to the type definitions) metrics.ts
R21, R22 MCP/skill classifier accepts only intended tools Wrong Fixed (allow-list) metrics.test.ts
R23 Redirections invalidate segments in every mode Wrong Fixed (reworded: granularity only) classifiers.test.ts
R24 Helper list includes base64 Wrong Fixed (code now includes it) shell.ts:SHELL_HELPERS
R26 Vars "in a gitignored .env" Wrong Fixed (code now loads .env) --env-file-if-exists=.env
R27 "Playwright ready out of the box" Unverifiable Fixed (prerequisites and install-browser step added) Fresh-clone run below
R28 verify-arms --arm Wrong Fixed (implemented) cli.ts
R30, R32 Playwright 0/20, 20/20, 20/20; 0/10, 10/10, 7/10; T1 1.64× Verified Verified experiments/playwright/runs/n5/findings.md
R31 Playwright baseline failures "(timeouts)" Partly wrong Fixed 17/20 T1 timed out, 10/10 T2
R33 Playwright T2 Skill/MCP 2.01× Wrong Fixed: 1.86× (report), 1.52× completed-only 3 zero-token MCP timeouts inflated it
R34 MCP cheaper because skill snapshots after every action Mechanism wrong Fixed Form: 25 fill vs 8 snapshot (skill); MCP 5 browser_fill_form
R35 MCP recovery "convergence failure" Partly wrong Fixed Stalled after ≤ 4 calls; cause undetermined
R36, R41, R42 issue_triage: MCP fan-out at 75+ turns; skill via gh issue list --label Wrong Fixed 403 on every issue call in both arms; turns 58–78
R37 Skill escapes surface on 2/3 GitHub T1 tasks Verified Verified 0/5 valid on repo_inventory, issue_triage
R38 "One trial" lifted the controller token Wrong Fixed All 5 skill passes used escalated credentials
R39 Fix "in ef3fc97" Wrong Fixed (90d0dd7, and noted it didn't take effect) git log
R40 Fix "behaviorally verified in env-fix-verify" Wrong Fixed That trial passed via the keyring login
R43 GitHub T2 table, directed 3/5 / 2/5 Verified Verified (+ "1/5 pass and valid") directed-n5/results
R44 Directed prompt "triples the cost" Wrong Fixed: 2.45× (passing), 2.22× (pass and valid) recomputed
R45 Clean GitHub ratios 1.13/1.32/1.31/1.38× Verified Verified findings tables
R46 Same identity for both tokens Unverifiable Unverifiable (kept as stated) not provable from repo
R47 Agent "could in principle walk /Users" Understated Fixed It did (436 calls); now denied
R48 Adding an experiment Verified Verified (updated for tools) experiment.ts
R49 Preflight points to a README section Wrong Fixed Message points to "Configuration"
— Repo description (N=5, three surfaces) Verified Verified
CLAUDE.md --allowed-tools restricts; scrub covers GH_*/GITHUB_* Wrong Fixed (code now makes the scrub claim true) runner.ts, isolation.test.ts
Docs X10/X11 Skill 15/25 in surface; MCP 20/25 in surface Wrong Fixed: skill 10/25, MCP 22/25 (re-classified) recomputed
Docs X5 Turn counts within ±2 Wrong Fixed (Playwright form/checkout differ) findings
Docs X20/X22 Workflow-status pre-fix 60 turns / 169.7 s Unverifiable Fixed: marked "not recorded" no result files
Docs P20 MCP tier2_recovery 70% Wrong Fixed: 40% 2/5
Docs H48 Race fix "in 64f9436" Wrong Fixed (e836d26) git log

3. Wording and numbers that changed

The headline results now come from n5-v2, a full re-run (161 trials) under the enforced isolation on Claude Code 2.1.288. The pinned subjects and model are the same as before. The May n5 run is kept as a "First run" section.

Use these now:

  • "MCP passed and stayed in surface on every task (13/13 across Playwright and GitHub)."
  • "MCP used fewer tokens on every task; the CLI skill cost 1.5–2.1× as much (2.67× on form-filling) under strict isolation."
    • Playwright Tier 1: 1.92×; Tier 2: 1.84×.
    • GitHub clean tasks: 1.52–1.77×.
  • "The CLI arm solved two tasks 5/5 but stayed in surface 0/5: it reached for base64 -d and shell variables where gh lacks a primitive. The result reproduced across both runs."
  • "Telling the agent the in-surface workaround didn't fix it: 0/5 trials passed in surface, and the passing trials cost 2.63× the tokens."

Replace the old figures:

Old claim Replace with
"1.13×–1.38×" (GitHub) "1.5–1.8×"
"1.64×" (Playwright) "1.9×"
"2.01×", "1.86×" (Playwright Tier 2) "1.84×"
"triples the cost" / "2.45×" (directed prompt) "2.63×, and 0/5 in surface"
"MCP collapses on issue triage", "skill solves it in one call" Drop both. Issue triage was a mis-scoped token in the first run; with a correct token both arms solve it 5/5 (1.67×).
"MCP tier2_recovery 2/5 / 70% / 40%" Drop it; MCP is 5/5 in n5-v2.
Security: "one trial lifted the controller token" "In the first run, every passing CLI trial on one task used credentials it shouldn't have had (controller token or the developer's gh login), because the environment scrub silently didn't apply; the harness now sandboxes the CLI arm and removes all inherited GitHub credentials."

Explain the ratio change honestly (optional). Ratios rose from the first run because each turn's fixed context shrank (fewer tool definitions, newer Claude Code), which helps the smaller MCP arm proportionally more. The direction held in both runs; the exact multiple depends on the setup.

4. Recommended repository settings (not applied; run these yourself)

R=chief-builder/cli-vs-mcp

# Ruleset on main: PR required, CI checks required, no force-push or deletion.
# 0 approvals because you are the only maintainer (you can't approve your own PR); raise it if you add collaborators.
gh api -X POST repos/$R/rulesets --input - <<'JSON'
{
  "name": "main",
  "target": "branch",
  "enforcement": "active",
  "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
  "rules": [
    { "type": "deletion" },
    { "type": "non_fast_forward" },
    { "type": "pull_request", "parameters": {
        "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": true,
        "require_code_owner_review": false, "require_last_push_approval": false,
        "required_review_thread_resolution": true } },
    { "type": "required_status_checks", "parameters": {
        "strict_required_status_checks_policy": true,
        "required_status_checks": [
          { "context": "Lint, typecheck, test", "integration_id": 15368 },
          { "context": "Link check", "integration_id": 15368 },
          { "context": "Analyze (javascript-typescript)", "integration_id": 15368 } ] } }
  ]
}
JSON

# Secret scanning + push protection
gh api -X PATCH repos/$R \
  -f 'security_and_analysis[secret_scanning][status]=enabled' \
  -f 'security_and_analysis[secret_scanning_push_protection][status]=enabled'

# Dependabot alerts and security updates
gh api -X PUT repos/$R/vulnerability-alerts
gh api -X PUT repos/$R/automated-security-fixes

# Private vulnerability reporting (SECURITY.md links to it)
gh api -X PUT repos/$R/private-vulnerability-reporting

# Topics and homepage
gh repo edit $R --homepage https://chief-builder.github.io/cli-vs-mcp/ \
  --add-topic claude-code,model-context-protocol,mcp,ai-agents,llm-evaluation,playwright,github-cli,typescript

# CodeQL: this PR adds an advanced workflow (.github/workflows/codeql.yml). Default setup and an advanced
# workflow can't both be active. If you prefer default setup, delete codeql.yml first, then:
#   gh api -X PATCH repos/$R/code-scanning/default-setup -f state=configured -f query_suite=default

The default branch is already main; no rename needed.

5. Open items not addressed

  • Your gh OAuth token was exposed in my local session log. During testing, an unsandboxed probe printed it. It never reached the repo or the PR. Please rotate it (gh auth refresh, or revoke it in Settings → Applications).

  • Git history still contains the personal data that 9450f9e redacted: your email, config-file fragments, and home-directory listings. Your ground rules forbid rewriting history. If you want it gone, you'd have to rewrite history yourself (git filter-repo) and force-push; forks and caches may keep copies. No credentials are in history (gitleaks: 0).

  • One tool-call argument names a private folder (~/Documents/Projects/mcp-sub-registry, in a Glob path). I kept it as escape evidence; it's a one-line edit to drop it.

  • Live GitHub verification: done (commit 29b7e6a, after the tokens were rotated).

    • The first live skill trial showed that gh couldn't verify TLS inside the macOS sandbox. Fixed with enableWeakerNetworkIsolation; the keychain and $HOME stay blocked (re-tested).
    • Trials in experiments/github/runs/isolation-check: all arms valid; skill and mcp pass Tier 1 pr_diff_answer and Tier 2 issue_create; baseline fails as expected.
  • Residual: baseline file tools can still read outside ~ and the repo (e.g. $TMPDIR). The results are redacted from artifacts. Full confinement needs a container.

  • Re-run done (n5-v2). 161 trials under the current isolation. It surfaced and fixed 4 harness issues: a setup repo leak (8a6cc40), unparsed tool input (40521c2), a short issue-create checker window (40521c2), and Tier 2 re-classification (8d113c0). One tier2_issue_create skill trial is a recorded checker false negative.

  • Playwright skill arm is not OS-sandboxed. It needs local browsers and a loopback server, so it relies on the classifier and file-tool deny rules; this is documented in SECURITY.md.

  • Coverage is 41% of lines overall. Core logic is well covered:

    File Lines
    shell 100%
    log 100%
    redact 97.5%
    fixtureServer 95%
    playwright spec 94%
    github spec 94%
    metrics 93%
    runner 90%
    report 77%
    trialState 79%

    The total is pulled down by three things:

    • The GitHub task files and provisioner.ts: they call the live GitHub API, and I didn't add an HTTP mock layer.
    • cli.ts: it is exercised in subprocess tests, which v8 coverage doesn't count.
    • The Playwright task success checks beyond tier1_scrape.
  • There's no build artifact. The harness runs from source via tsx, so CI's "build" step is a CLI smoke test (pnpm harness --help).

  • Apache-2.0 change notice for the skill is in NOTICE rather than inside SKILL.md, because SKILL.md is the experimental stimulus.

  • Redesign proposals not done:

    • Run each trial in a container (one boundary for every arm, Playwright included).
    • Pin the Claude Code version per run.
    • Record the negotiated MCP protocol version in results.
  • .claude/skills/codex-imagegen/ was untracked in your working tree before this work. I left it alone and didn't commit it.

6. Phase 3 verification (fresh clone of this branch)

Toolchain and checks:

$ node -v && pnpm -v && claude --version
v24.15.0
12.8.1
2.1.287 (Claude Code)
$ pnpm install
Done in 295ms using pnpm v12.8.1
$ pnpm check
$ tsc --noEmit
$ eslint .
$ prettier --check .
All matched files use Prettier code style!
$ vitest run
 Test Files  8 passed (8)
      Tests  80 passed (80)
$ pnpm test:coverage
      Tests  80 passed (80)
All files          |   40.76 |    38.91 |   42.16 |    41.1 |

Quickstart, run as written in the README:

$ pnpm exec playwright-cli install-browser
WebKit 26.4 (playwright webkit v2287) downloaded to ~/Library/Caches/ms-playwright/webkit-2287
$ pnpm harness verify-arms --experiment playwright        (tool lists abbreviated)
ARM: baseline  built-in tools (5): Glob Grep Read ToolSearch Write   MCP tools (0): (none)
ARM: skill     built-in tools (4): Bash Skill ToolSearch Write        MCP tools (0): (none)
ARM: mcp       built-in tools (2): ToolSearch Write                   MCP tools (23)   MCP servers: playwright:connected
$ pnpm harness run --experiment playwright --run quickstart --arm skill --task tier1_scrape --trials 1
  ✓ score=1.00  valid  tokens_in=8 turns=8 time=16.1s
$ pnpm harness run --experiment playwright --run quickstart --arm mcp --task tier1_scrape --trials 1
  ✓ score=1.00  valid  tokens_in=9 turns=9 time=9.1s
$ pnpm harness report --experiment playwright --run quickstart
| tier1_scrape | 1 | skill | 1 | 0 | 100% | 100% | 100% | 1.0 | 8 | 69323 | 11060 | 739 | 81130 | 4.0 | 8.0 | 16.1s |
| tier1_scrape | 1 | mcp   | 1 | 0 | 100% | 100% | 100% | 1.0 | 9 | 39077 | 6977  | 643 | 46706 | 4.0 | 9.0 | 9.1s  |

Other checks:

  • Link check (lychee, same config as CI): 47 links, 0 errors, 3 excluded.
  • Rendered task prompts were hashed before and after the lint/format changes: all 14 are byte-identical.

CI on this PR (head 896e5c7): all green.

  • Lint, typecheck, test: pass
  • Link check: pass
  • CodeQL Analyze (javascript-typescript): pass
  • CodeQL Analyze (actions): pass
  • CodeQL code-scanning check: "No new alerts"

The first CI run flagged 4 CodeQL alerts:

  • 1 high: js/path-injection, fixture server.
  • 2 medium: js/stack-trace-exposure, fixture server (3 occurrences).
  • 1 in an agent-written script inside the committed run artifacts.

Commit 896e5c7 fixes them: the path check uses path.resolve plus a containment test, errors return generic 413/500 bodies, and run artifacts are excluded from CodeQL.

- engines node >=24 <25, .nvmrc, packageManager pnpm@12.8.1
- typescript 7, tsx 4.23, commander 15, execa 10, zod 4.6, @types/node 24
- drop the package.json pnpm field (ignored by pnpm 12; pnpm-workspace.yaml
  allowBuilds already covers esbuild)
- tsconfig: noEmit; drop unused jsx/declaration/outDir options
- experiment subjects (@playwright/cli, @playwright/mcp, github-mcp-server)
  stay pinned so new runs remain comparable with committed data
- eslint (typescript-eslint recommended) and prettier with a config that
  matches the existing style; run artifacts, fixtures and docs excluded
- remove unused readJsonIfExists (playwright tier2) and mkSeed (trialState)
- drop a no-op `\$` escape in the directed-prompt template; the rendered
  prompt strings for all 14 tasks were hashed before and after and are
  byte-identical
- hold TypeScript at 6.0.3: typescript-eslint 8.71 refuses TS 7.0
Isolation
- pass a positive built-in tool list per arm with `claude --tools`;
  `--allowed-tools` never restricted anything under bypassPermissions
- deny Read/Edit of ~ and the repo via --settings, so file tools can't
  reach the developer's home directory or other trials' answers
- run the GitHub skill arm's Bash in Claude Code's OS sandbox with no
  unsandboxed fallback (blocks the gh keyring login and $HOME reads)
- classifier is now an allow-list derived from the same per-arm list;
  MCP resource tools count as MCP surface, planning tools as neutral

Credentials
- execa merged process.env back into the child (extendEnv defaulted to
  true), so the GH_/GITHUB_ scrub, including 90d0dd7, never removed
  anything; pass extendEnv: false and scrub by prefix
- give each trial an empty GH_CONFIG_DIR

Measurement
- timed-out trials recorded 0 tokens and 0 ms; sum per-message usage as
  a flagged lower bound and use the harness-measured duration
- report gains a Timeouts column and a note on estimated tokens

Robustness and config
- task cleanup (sandbox repo deletion) and temp-dir removal now run in
  finally; setup failures no longer leak repos or dirs
- central config.ts (model, timeout, tool lists) and zod-validated
  GitHub env; .env is loaded by the harness script
- CLI validates --trials/--tier/--arm/--run and reports unknown
  experiments without a stack trace
- verify-arms runs each arm in a trial tempdir and prints the tool list
  from the init event; gains the documented --arm option
- redact-artifacts command and redaction of home paths and out-of-trial
  file-tool results on every transcript the runner writes
- small logger with LOG_FORMAT=json and token redaction
- shared helper-word pattern; Playwright no longer flags helper names
  inside URLs/JS, and both classifiers now treat base64 as a helper
- classifiers, shell parsing, metrics (incl. timeout estimation), env
  scrub and token routing, trial settings, config validation, logger
  redaction, fixture server (traversal, 413, 405), redaction, seeds
- end-to-end runTrial with a fake claude executable over the real
  tier1_scrape fixture: scoring, redaction, cleanup, timeout path
- CLI argument validation and GitHub preflight
- pnpm test / pnpm test:coverage
Success-check notes quote output paths under the repo, which live under
the home directory. Found in a live smoke run.
Redaction (pnpm harness redact-artifacts, plus two literal edits)
- home directory paths -> ~ in all 306 affected artifact files
- results of baseline file-tool calls outside the trial directory
  (~/.gitconfig, ~/.claude.json, shell rc files, gh config, home-dir
  listings) replaced with a marker; the calls themselves are kept as
  escape evidence
- the author's email quoted in two transcripts' reasoning text replaced
  with <redacted-email>
No credentials were present (gitleaks: 0 findings in tracked files).

Recompute (pnpm harness recompute-metrics; github-rw for Tier 2 runs)
- 65 timed-out trials now carry lower-bound token estimates and the
  timeout as wall clock instead of 0 / 0
- allow-list classifier flips 4 trials to invalid surface, all failed
  trials: 3 GitHub n5 mcp tier1_issue_triage (Grep/Glob) and 1
  Playwright n5 baseline tier1_login (TaskOutput)
- no pass/fail values changed
- regenerate the report tables in five findings.md files and both smoke
  report.md files from the recomputed results; narratives kept
- GitHub n5: issue_triage was a token-permission failure (403 on every
  issue call in both arms), and all 5 skill passes used escalated
  credentials (controller token or the developer's gh login); the
  env-scrub change did not take effect at the time; commit 64f9436 ->
  e836d26; MCP turns 58-78, not 75+
- Playwright n5: Tier 2 ratio 2.01x -> 1.86x (1.52x completed trials),
  tier2_recovery MCP 40% not 70%, cost mechanism and recovery failure
  description corrected against transcripts
- directed-n5: 2.6x -> 2.45x, escape-rate wording, invalid-trial count,
  compliant-trial walkthrough
- tier2-n5, workflow-n5, smoke findings: dependent claims corrected
Each corrected narrative carries a dated correction note.
README: summary, why it matters, Mermaid architecture and module table,
isolation as implemented, prerequisites and a quickstart verified from a
clean clone, configuration and command tables, tasks, corrected N=5
results, status and limitations, development commands, license.

Claims removed or corrected: FNV-1a paired seed (it is SHA-256),
--allowed-tools as a restriction, verify-arms --arm (now implemented),
.env loading (now implemented), numTurns in the schema, redirections
invalidating segments, ef3fc97, 2.01x, 75+ turns, 'triples the cost',
and the issue_triage fan-out story.

CLAUDE.md: describe --tools, deny rules and sandbox; env scrub with
extendEnv false; redaction; don't point report --output at findings.md
- remove the v1 pages (cli_mcp_experiments, github_experiment,
  playwright_experiment) and every link to them; index.html already
  redirects to the v2 overview
- add a "Corrections (October 2026)" note to the overview, linked from
  the GitHub and Playwright pages
- issue_triage: token-permission failure (403s), escalated credentials
  in all skill passes; MCP turns 58-78; pre-Actions workflow numbers
  marked as not recorded
- Playwright: Tier 2 ratio 1.86x (1.52x completed trials), recovery 40%,
  cost explained by per-field fill vs batched browser_fill_form, recovery
  failures described as stalls with no determined cause
- directed prompt: pass-and-valid 1/5, about 2.2-2.5x tokens
- isolation described as implemented now, noting n5 predates it
- remove two images that illustrated disproven claims
  (playwright-cycle-cost.png: action-then-snapshot loop;
  surface-escape.png: fan-out into timeouts)
- LICENSE (MIT); NOTICE and third_party/playwright-cli/LICENSE for the
  Apache-2.0 skill adapted from @playwright/cli, with its changes listed
- SECURITY.md (private reporting, supported versions, threat model),
  CONTRIBUTING.md, CHANGELOG.md for 1.1.0, .editorconfig, .env.example
- issue and PR templates
- .gitignore covers .env.*, editor files, coverage
- fake test tokens labelled; test/ excluded from GitHub secret scanning
  and gitleaks
- version 1.1.0; prettier formatting of .mcp.playwright.json
- CI on push to main and pull requests: frozen install, lint, format
  check, typecheck, tests with coverage (summary in the job summary),
  CLI smoke, and a lychee link check of markdown and the docs site
- CodeQL for javascript-typescript and actions, weekly schedule
- top-level permissions contents: read; security-events: write only on
  the CodeQL job; actions pinned to commit SHAs; concurrency groups;
  per-job timeouts; checkout without persisted credentials
- Dependabot for npm (grouped dev tooling) and GitHub Actions; the
  @playwright/cli experiment subject and TS/@types/node majors ignored
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

- resolve request paths with path.resolve and require them to stay under
  the root (CodeQL js/path-injection); test a sibling-prefix traversal
- return generic 413/500 bodies instead of echoing error strings
  (js/stack-trace-exposure)
- exclude committed run artifacts (agent-written scripts) from CodeQL
Under Claude Code's Bash sandbox, Go CLIs like gh fail TLS verification
on macOS (x509: OSStatus -26276), so the GitHub skill arm couldn't use
gh at all and fell back to curl. Set sandbox.enableWeakerNetworkIsolation,
the documented setting for this; re-verified that the keychain and $HOME
stay blocked.

Adds the live isolation-check run (Tier 1 pr_diff_answer x3 arms, Tier 2
issue_create x2 arms) as evidence, documents the remaining $TMPDIR read
reach, and corrects the audit's earlier sandbox claim.
The runner only calls Task.cleanup once setup returns state, so a setup
that failed after creating its repo (seen in n5-v2: workflow_status
couldn't list Actions runs) leaked the repo, and the next arm's trial
for the same seed then failed with 'name already exists'.

- provisionRepo seeds inside deleteOnFailure
- tier1_pr_diff_answer and tier1_workflow_status run their
  post-provision steps inside deleteOnFailure
- tests with a mocked GitHub API; integration cleanup assertion now
  checks this trial's own dirs so it is safe alongside live runs
Rendered prompts unchanged.
- Claude Code 2.1.288 sometimes records tool input as
  {__unparsedToolInput: {raw}}; the parser saw no command and flagged
  ordinary gh calls as escapes (2 n5-v2 workflow_status trials). Unwrap
  raw (with a fallback for truncated JSON) before classifying.
- tier2_issue_create's success check listed issues 6x500 ms; GitHub's
  list lagged a correct create in n5-v2 (false negative). Now 12x1 s.
Rendered prompts unchanged.
…ub-rw

n5-v2 stores Tier 1 (read-only) and Tier 2 (read-write) GitHub results in
one run; each tier must be re-classified with its own spec.
Claude Code 2.1.288, claude-sonnet-4-6, @playwright/cli 0.1.13,
@playwright/mcp 0.0.75, github-mcp-server v1.0.4; harness 29b7e6a
(tier1_workflow_status re-run on 8a6cc40 after the controller token
gained Actions: read). skill/mcp N=5, baseline N=2; directed variant
skill N=5. 161 trials. Metrics recomputed with 40521c2 (unparsed tool
input); GitHub Tier 2 classified with github-rw. Lane logs record
versions. No tokens, home paths, or leftover sandbox repos.
- README Results: n5-v2 headline (MCP clean on every task; skill/MCP
  1.5-2.1x, form 2.67x; base64 escapes reproduced; issue_triage valid
  at 1.67x; directed 0/5 pass+valid) and a First run (n5) section that
  explains what changed and why ratios rose
- n5-v2 findings with narrative for both experiments
- GitHub setup documents read-only and read-write agent tokens
- CHANGELOG and AUDIT addendum for the re-run and the fixes it surfaced
- hero metrics, findings, tables and charts from the n5-v2 data: MCP
  clean on all 13 tasks; skill/MCP 1.5-2.1x (form 2.67x); skill 0/10
  valid on repo_inventory and file_patch_pr; issue_triage 1.67x;
  recovery 5/5; directed 0/5 pass+valid at 2.63x tokens
- bar charts recomputed (largest skill bar = 100%: Playwright checkout
  266,073; GitHub issue_workflow 100,251)
- the October corrections note becomes a 'First Run (May 2026)' section
  (anchor #first-run, old #corrections anchor kept) with n5 figures and
  why they differ; per-run setup described
@chief-builder
chief-builder merged commit fee9ad4 into main Oct 3, 2026
5 checks passed
@chief-builder
chief-builder deleted the hardening/2026-10-02 branch October 3, 2026 02:02
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.

2 participants