Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
187 commits
Select commit Hold shift + click to select a range
d8bcaad
feat(c-ffi): post-and-drain async callback C API (phase 2) (#495)
DorianZheng May 9, 2026
e2195ba
rest: treat BoxID as opaque server-issued identifier (#498)
DorianZheng May 10, 2026
bdf888f
ci(e2e): make runner instance persistence non-fatal (#499)
DorianZheng May 10, 2026
9d57486
ci(e2e): expand subnet pool to all AZs in the VPC (#500)
DorianZheng May 10, 2026
2daae30
ci(e2e): move make setup from user-data to a job step (#501)
DorianZheng May 10, 2026
da729e9
chore(deps): bump github.com/go-git/go-git/v5 (#503)
dependabot[bot] May 12, 2026
9c458bc
chore(deps): bump @opentelemetry/sdk-node (#504)
dependabot[bot] May 12, 2026
236b76d
feat(exec): runner attach controller + env/workdir/timeout plumbing (…
DorianZheng May 12, 2026
e2039a4
feat(release): curl|sh installer + SHA256SUMS + build provenance (#506)
DorianZheng May 12, 2026
45e211f
chore(release): bump SDK patch versions to 0.9.4 (#509)
DorianZheng May 12, 2026
be75a4a
ci(e2e): rescue logs, disk precheck, attempt-namespaced artifacts (#508)
DorianZheng May 12, 2026
16cca41
feat(release): sh.boxlite.ai Cloudflare Worker for installer (#510)
DorianZheng May 12, 2026
bde5e5b
docs(cli): add CLI reference + README CLI Quick Start (#511)
DorianZheng May 12, 2026
2628ca5
Fix mobile credential row overflow (#513)
DorianZheng May 13, 2026
8759bc1
chore(deps): bump astro (#514)
dependabot[bot] May 14, 2026
5b3b89e
fix(runner): pong-based liveness for WebSocket attach sessions (#516)
DorianZheng May 14, 2026
2b60703
chore(deps): add lint:yarn-lock make target + pre-commit hook (#517)
DorianZheng May 14, 2026
f04b438
fix(dashboard): constrain dialog grid/flex children so long values st…
DorianZheng May 14, 2026
0cb2290
feat(dashboard): mobile-first sandbox terminal and VNC (#521)
DorianZheng May 14, 2026
f052b69
fix(runner): SSH gateway uses BoxLite exec (ssh -p 2222 back online) …
DorianZheng May 14, 2026
6acdb2b
fix(dashboard): RP-initiated logout fallback for non-compliant IdPs (…
DorianZheng May 14, 2026
e4767e7
fix(runtime): preserve box record on init failure as Failed state (#520)
DorianZheng May 14, 2026
b8fed57
feat(api): single bearer auth, /v1/me, RFC 8628 device flow endpoints…
DorianZheng May 14, 2026
86c1fa3
feat(api): drop OAuth device-flow endpoints + schemas from spec (#531)
DorianZheng May 15, 2026
5e7ec62
fix: move test cache under workspace target (#533)
uran0sH May 15, 2026
e9a4952
feat(auth): bearer auth + RFC 8628 device flow (SDK + CLI + server st…
DorianZheng May 16, 2026
625bbca
test: keep-going matrix via FAIL_FAST + FILTER for every suite (#534)
DorianZheng May 16, 2026
170d926
refactor(node): move rest bag adaptation into the napi binding (#536)
DorianZheng May 16, 2026
7f4c42b
fix(runtime): prune embedded cache by each dir's own build profile (#…
DorianZheng May 16, 2026
f22b1d4
chore(sdk): bump SDK patch version 0.9.4 -> 0.9.5 (#538)
DorianZheng May 16, 2026
cac620c
test(security): GHSA-g6ww-w5j2-r7x3 Python regression + advisory note…
DorianZheng May 16, 2026
7f8df26
test(security): GHSA-f396-4rp4-7v2j Python regression + advisory note…
DorianZheng May 16, 2026
28159fc
fix(guest): exec timeout watcher must SIGKILL, not SIGALRM by GHSA-xj…
G4614 May 19, 2026
6e447a9
chore(deps): bump github.com/go-git/go-git/v5 (#553)
dependabot[bot] May 20, 2026
86e1f7f
feat(auth): REST identity via BoxliteRuntime::auth() handle (#554)
DorianZheng May 20, 2026
28c09e1
chore(make): workspace-wide apps targets + drop yarn-lock-sync hook (…
DorianZheng May 20, 2026
12689d5
feat(serve): optional exact-key auth for serve + reference-server (#557)
DorianZheng May 20, 2026
51004a1
fix(test-python): use shared_runtime fixture to avoid BOXLITE_HOME fl…
G4614 May 20, 2026
d8b58f4
test(security): Node + Go regression parity for GHSA-g6ww + GHSA-f396…
DorianZheng May 20, 2026
1711d8c
refactor(logging): executable-owned subscriber + layered CLI sink (#562)
DorianZheng May 20, 2026
0bfd2f5
feat(api-key): Stripe-style {prefix}_{class}_{base62(32)}{crc6} forma…
DorianZheng May 20, 2026
93a1f40
chore(infra): track .env.example for stack setup (#566)
DorianZheng May 21, 2026
dcb3df4
feat(auth): URL prefix authoritative + OIDC device flow (#581)
DorianZheng May 23, 2026
acaa55f
chore(workspaces): track empty CLAUDE.md placeholders across the tree…
DorianZheng May 23, 2026
220416d
chore(deps): bump rusqlite 0.37 -> 0.39 (#582)
DorianZheng May 23, 2026
54531e9
chore(deps): bump the npm_and_yarn group across 2 directories with 2 …
dependabot[bot] May 23, 2026
6129e92
chore(deps): bump github.com/boxlite-ai/boxlite/sdks/go (#570)
dependabot[bot] May 23, 2026
e90fc8a
fix(quality): lint:python tomllib check works on Python 3.10 (#574)
G4614 May 23, 2026
8e8a679
fix(security): paired redaction — stop CA key/secret leaks via trace!…
G4614 May 23, 2026
b2e1f99
fix(auth): treat empty BOXLITE_API_KEY as unset in `auth status` (#577)
G4614 May 23, 2026
f70ee07
ci(e2e): auto-evict caches at >=80% root use instead of hard-fail (#584)
DorianZheng May 23, 2026
11af5d7
feat(rest): typed BoxliteError → HTTP mapping with snake codes + requ…
DorianZheng May 23, 2026
4307226
fix(recovery): PID-fingerprint identity + crash-aware attach pipeline…
DorianZheng May 25, 2026
bc8cd43
test(auth): drop stale period in status "Not logged in" assertion (#598)
G4614 May 27, 2026
f523f13
chore(claude): add CLAUDE.md preflight hook + auditor subagent (#599)
DorianZheng May 27, 2026
2d42334
docs: correct BoxOptions::detach contract + restore two-side test rul…
DorianZheng May 27, 2026
1a84cca
fix(spawn): set session leader for detached, own pgroup for non-detac…
DorianZheng May 27, 2026
09ba702
chore(claude): add PR-review acknowledgment PreToolUse hook (#603)
DorianZheng May 27, 2026
2b9b545
test(test-utils): PerTestBoxHome::Drop fails loudly if a shim is stil…
DorianZheng May 27, 2026
5d717f5
fix(test): add missing .clone() in image_registries test after #604 (…
G4614 May 27, 2026
9883279
fix(images): defer dir chmod across layers to fix overwrite EACCES (#…
DorianZheng May 27, 2026
2eec913
fix(setup): install cmake across platform scripts for C SDK tests (#609)
DorianZheng May 28, 2026
7e37c9e
fix(make): propagate test failures instead of masking via POSIX ; cha…
G4614 May 28, 2026
d5b93da
test(health_check): explicit stop() so the healthy-box test doesn't l…
G4614 May 28, 2026
c5f508f
fix(runner): roll back SignaledHUP/TERM when SDK signal delivery fail…
G4614 May 28, 2026
6048e91
fix(libcontainer): bump to youki rev — InitReady race + per-exec moun…
G4614 May 28, 2026
71d0986
[urgent] fix(guest): declare terminal=true for TTY execs (libcontaine…
G4614 May 30, 2026
c0bcf0a
fix(test:apps): inject boxlite_dev tag via GOFLAGS for apps Go tests …
G4614 Jun 1, 2026
694b4e2
fix(net): surface gvproxy bind errors through FFI (#612)
G4614 Jun 1, 2026
0bf0b3a
fix(disk,runtime): cross-fs safe disk + staging moves (EXDEV fallback…
G4614 Jun 1, 2026
a438d6b
fix(cli): preserve profile path_prefix when BOXLITE_API_KEY is set (#…
G4614 Jun 1, 2026
6802ca7
fix(api): source /v1/me expires_at from ApiKey.expiresAt (P1-2) (#649)
lilongen Jun 3, 2026
6684b88
chore: configure CLA assistant (#650)
DorianZheng Jun 3, 2026
f21e5a4
chore(deps-dev): bump the npm_and_yarn group across 2 directories wit…
dependabot[bot] Jun 3, 2026
2d02d76
feat: upgrade MiniMax default model to M3 (#643)
octo-patch Jun 4, 2026
af5d099
docs: add communication rules to CLAUDE.md (#653)
DorianZheng Jun 5, 2026
71e1754
infra: configurable pgAdmin exposure (secure default) + tiered .env.e…
DorianZheng Jun 5, 2026
fe557e7
feat(infra): RUNNERS-count multi-runner provisioning; drop dead RUNNE…
DorianZheng Jun 6, 2026
2337d6b
fix qcow2 backing and container status checks (#525)
GatewayJ Jun 8, 2026
92c7308
fix(cli): stop box before hard-exit on non-zero run exit code (#622)
G4614 Jun 8, 2026
04bc143
test(sdk-typescript): pass jest target when no test files exist (#617)
G4614 Jun 8, 2026
7a10d8f
test-stress(disk-guest): box rootfs is a bounded, isolated disk that …
G4614 Jun 8, 2026
b302fce
test(cli): box lifecycle journey suite — birth-to-death + cp/stats ga…
G4614 Jun 8, 2026
8a21b8c
fix(cli): read stdin on a dedicated OS thread so TTY exec exits witho…
G4614 Jun 8, 2026
87aefad
fix(api): anchor OIDC-created user's Personal org to the default regi…
lilongen Jun 8, 2026
d46ae98
fix(api): reject undersized box resources at the create boundary (#662)
lilongen Jun 8, 2026
2e1c6e1
fix(runner): pull snapshot layers for the host arch, not hardcoded am…
lilongen Jun 8, 2026
5e08966
ci: prep test/lint workflows for a merge queue (#677)
DorianZheng Jun 8, 2026
f6e934b
ci: run test/lint on every PR so required conclusion checks always re…
DorianZheng Jun 8, 2026
e4bbd0e
test(e2e): SDK→API→Runner→VM regression suite (#678)
G4614 Jun 8, 2026
8007f71
fix(disk): build ext4 rootfs from trees with unreadable (0000) files …
DorianZheng Jun 9, 2026
41bd185
chore(codeowners): map subsystem ownership to maintainers/builder tea…
DorianZheng Jun 9, 2026
0b0ba91
docs(readme): add BoxLite logo banner (#707)
law-chain-hot Jun 9, 2026
3b0675e
test(e2e): add error-mapping, quota, runner-concurrency cases (xfails…
G4614 Jun 9, 2026
56bba3e
fix(rest): extract uploaded tar before CopyInto in runner file upload…
G4614 Jun 9, 2026
e3946ff
fix(rest): copy_out single-file mode writes at host_dst, not inside i…
G4614 Jun 9, 2026
7b0cc0d
ci(codeql): switch to advanced setup so fork PRs are scanned (#708)
DorianZheng Jun 9, 2026
490d093
refactor: rename Sandbox -> Box (Part 1: apps/api epicenter) (#706)
law-chain-hot Jun 9, 2026
7f68dce
fix(apps): repair ESLint flat config lint (#713)
law-chain-hot Jun 10, 2026
2d6ae82
fix(go-sdk): fold stream drain into Execution.Wait (os/exec io.Writer…
G4614 Jun 10, 2026
8f8629c
chore(apps): restore Daytona API-client regen machinery + regenerate …
DorianZheng Jun 10, 2026
e9ae0e6
chore(deps): bump the npm_and_yarn group across 1 directory with 2 up…
dependabot[bot] Jun 10, 2026
8a62978
docs(cli,daemon): regenerate cobra docs and toolbox swagger after ren…
DorianZheng Jun 10, 2026
53a81ea
fix(api,e2e): finish Sandbox → Box rename — 3 schema gaps + fixture c…
G4614 Jun 10, 2026
e526b6c
chore(apps): rename analytics-api-client Sandbox -> Box via in-repo s…
DorianZheng Jun 10, 2026
04a7f8b
test(e2e): add cli-detach-recovery, exec-attach, volume-readonly case…
G4614 Jun 10, 2026
9a438c1
feat(infra-local): BoxLite-based local dev stack (L1 + L2 + M5 native…
lilongen Jun 10, 2026
2d93cc3
chore(apps,e2e): sweep Sandbox→Box rename leftovers — regen toolbox c…
DorianZheng Jun 10, 2026
577f4a5
Converge A2 + MVP box journey into main (super PR — review & split) (…
law-chain-hot Jun 10, 2026
e2f7ca8
chore(apps): regenerate API clients against merged API surface (#715 …
DorianZheng Jun 10, 2026
460c5d6
chore(apps): adapt cli, dashboard, sdk-typescript to the regenerated …
DorianZheng Jun 10, 2026
014914a
chore(apps): remove the Go CLI (apps/cli) (#728)
DorianZheng Jun 11, 2026
21df05a
fix(exec): preserve UTF-8 codepoints split across attach chunks (#665)
ldarren Jun 11, 2026
baebe20
[codex] Fix dashboard box API contract drift (#729)
law-chain-hot Jun 11, 2026
5ae4b50
[codex] Normalize monorepo Docker build paths (#730)
law-chain-hot Jun 11, 2026
55936ed
fix(apps): vend box S3 credentials from the ECS task role, not a stat…
DorianZheng Jun 11, 2026
4a38492
fix(build): repair #715 runner build break and org test mock (#734)
law-chain-hot Jun 11, 2026
bd8c7f7
chore(db): squash pre-launch migrations into baseline (#736)
law-chain-hot Jun 11, 2026
e512bd2
chore(deps): bump @grpc/grpc-js from 1.14.3 to 1.14.4 in /apps in the…
dependabot[bot] Jun 11, 2026
ba7458d
fix(sdk): make boxlite_options_add_port carry the full PortSpec (#741)
DorianZheng Jun 11, 2026
20873af
fix(runtime): BoxSockets — bind box sockets via short /tmp symlinks (…
DorianZheng Jun 12, 2026
ad155d9
chore(apps): remove TypeScript cloud SDK (re-apply #738 onto main) (#…
law-chain-hot Jun 12, 2026
0cc2dda
Revert "chore(apps): remove TypeScript cloud SDK (re-apply #738 onto …
law-chain-hot Jun 12, 2026
6f96da3
feat(box): restore image-keyed warm-pool reuse and boot path (#755)
DorianZheng Jun 13, 2026
d035ad2
refactor(box): drop the dead userId field from the runner payload (#757)
DorianZheng Jun 13, 2026
707fb80
feat(box): restrict box creation to the supported pinned images (#758)
law-chain-hot Jun 13, 2026
5db267d
fix(runner): drop dead-on-arrival toolbox HTTP readiness check (#754)
G4614 Jun 15, 2026
43397aa
chore(dashboard): delete dead Playground and VNC UI (#760)
law-chain-hot Jun 15, 2026
fc560eb
chore(ci): rename e2e-test workflow and label to e2e-local (#764)
DorianZheng Jun 15, 2026
92f1ef3
refactor(dashboard): route box lifecycle through Box API (#761)
law-chain-hot Jun 15, 2026
40ba346
fix(api): validate Box API network allowlist (#775)
law-chain-hot Jun 15, 2026
c251121
refactor(api): remove deprecated box-operation surfaces (#762)
law-chain-hot Jun 15, 2026
6d3c8a7
[codex] Unify SST service Docker path roots (#780)
law-chain-hot Jun 15, 2026
3386730
fix(seatbelt): narrow macOS sandbox write grants under built-in profi…
G4614 Jun 15, 2026
07fa30f
[codex] Fix API image production build blockers (#782)
law-chain-hot Jun 15, 2026
f56beec
[codex] Fix API runtime proxy dependency (#787)
law-chain-hot Jun 15, 2026
8b0b89e
chore(infra-local): consolidate local-stack state into .apps-local/ +…
DorianZheng Jun 15, 2026
5575072
chore(infra-local): drop dead DYLD/snapshot config + pin boxlite CLI …
DorianZheng Jun 15, 2026
5f93780
feat(cli): expose --disk-size / --network / --entrypoint flags (#620)
G4614 Jun 15, 2026
6afe962
fix(infra): lock down service exposure, runner SG/IMDS, and API CORS …
DorianZheng Jun 15, 2026
82abc8b
Add supported box image picker (#792)
law-chain-hot Jun 15, 2026
89c8882
fix(infra): harden control-plane durability, deploy safety, config hy…
DorianZheng Jun 15, 2026
757692a
Use public curated image tags (#795)
law-chain-hot Jun 15, 2026
cf69a05
refactor(infra-local): simplify + unify into one Python `compose` pac…
DorianZheng Jun 16, 2026
f394033
chore(deps-dev): bump vite from 8.0.5 to 8.0.16 in /apps in the npm_a…
dependabot[bot] Jun 16, 2026
a1740c6
Use cloud Box ID as primary identity (#800)
law-chain-hot Jun 16, 2026
075f6a6
feat(dashboard): same-origin /api proxy for local dev against any API…
law-chain-hot Jun 16, 2026
011e52a
fix(infra-local): register the CLI auth-login loopback redirect URI i…
DorianZheng Jun 16, 2026
d9b36fe
fix(cli): surface cloud-exec stream failures instead of silent succes…
DorianZheng Jun 16, 2026
d83526c
fix(dashboard): recover from a 401 by bouncing to a fresh login (#806)
DorianZheng Jun 16, 2026
5c0b156
test(e2e): bring Tokyo e2e suite to green (split from #724) (#810)
G4614 Jun 16, 2026
89b88be
fix(api/box): reject per-box cpu/memory/disk over the org limit (#811)
G4614 Jun 16, 2026
dca17c3
feat(infra): sst.Secret + SSM for team secret management (#814)
DorianZheng Jun 16, 2026
9a74191
feat(dashboard): box table cleanup — drop region, tighten density, fu…
law-chain-hot Jun 16, 2026
75e78ac
feat(api): WS attach accepts OIDC JWTs, not just API keys (closes #80…
DorianZheng Jun 16, 2026
8303680
chore(deps): bump the npm_and_yarn group across 1 directory with 6 up…
dependabot[bot] Jun 16, 2026
fa7f0c4
feat(infra): name default runner boxlite-runner-default, fix extra-ru…
DorianZheng Jun 17, 2026
9e153f8
fix: re-PR functional bits of #808 against main (stopped-box exec 409…
G4614 Jun 17, 2026
0635fb7
test: add REST API E2E auth matrix and coverage tooling (#746)
G4614 Jun 18, 2026
bfa5b95
feat(security): default sandbox on + expose SecurityOptions to CLI/Go…
G4614 Jun 19, 2026
1fb8cb6
fix(api): transfer non-JSON runner response to standard JSON format (…
G4614 Jun 19, 2026
2baa275
chore(deps): bump the npm_and_yarn group across 1 directory with 4 up…
dependabot[bot] Jun 20, 2026
77e932c
chore(deps): bump the npm_and_yarn group across 1 directory with 4 up…
dependabot[bot] Jun 21, 2026
4a52285
docs(ci): add e2e-local CI runbook, de-stale workflow docs (#835)
DorianZheng Jun 22, 2026
cad894b
docs: add failure-class sweep principle to CLAUDE.md (#838)
DorianZheng Jun 23, 2026
8841340
fix(guest): reset SIGPIPE to SIG_DFL for container execs so a closed-…
G4614 Jun 23, 2026
6fff778
dashboard: mobile adaptation + restore web terminal + env-aware API U…
law-chain-hot Jun 24, 2026
dbb65c0
test: add API stress tests (#823)
G4614 Jun 24, 2026
5316b17
fix(api): auto-reconcile v2 boxes stuck in ERROR back toward STARTED …
G4614 Jun 24, 2026
0f5a42a
fix(runtime): rebuild invalid guest rootfs overlay (#578)
G4614 Jun 24, 2026
a433b66
fix(jailer): unblock the default sandbox (drop host-side Landlock + b…
G4614 Jun 24, 2026
1885e1d
chore(release): bump version to 0.9.7 (#844)
DorianZheng Jun 24, 2026
2d6e362
dashboard: 6 UI/UX fixes (status colors, stat refresh, action transit…
law-chain-hot Jun 25, 2026
4cbbbc4
dashboard: hide table columns, drop terminal paste button, loading ty…
law-chain-hot Jun 25, 2026
dc576df
fix(images): fail closed on DiffID/layer count mismatch (#772)
G4614 Jun 25, 2026
8007a86
fix(jailer): set LD_LIBRARY_PATH in bwrap sandbox so the shim can dlo…
G4614 Jun 25, 2026
2da97ab
fix(rest): client-initiated WS keepalive for idle cloud exec sessions…
DorianZheng Jun 25, 2026
199b625
fix(jailer): skip --die-with-parent for detached boxes (#851)
G4614 Jun 25, 2026
846ed7b
test(cli): avoid fixed publish ports (#854)
G4614 Jun 25, 2026
4f8d6b3
fix(jailer): bind /etc/resolv.conf so allow_net DNS works under the s…
G4614 Jun 25, 2026
a2a41c6
Add project hook registration (#858)
DorianZheng Jun 25, 2026
a3e183c
perf(api): long-cache hashed dashboard assets, no-cache the HTML shell
law-chain-hot Jun 24, 2026
cba0d40
perf(dashboard): persist OIDC session + cache session-stable shell qu…
law-chain-hot Jun 24, 2026
01cbf55
perf(dashboard): code-split non-first-paint routes out of the entry b…
law-chain-hot Jun 24, 2026
78b608d
perf(dashboard): de-suspend org members so the boxes table paints first
law-chain-hot Jun 24, 2026
c5fb2d4
perf(dashboard): correct two inaccurate comments (review feedback)
law-chain-hot Jun 25, 2026
9a0b292
perf(dashboard): trim first-paint bundle (lazy prism + defer algolia)
law-chain-hot Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
49 changes: 49 additions & 0 deletions .claude/agents/claude-md-auditor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: claude-md-auditor
description: Independent auditor that judges a pending git commit or push against every applicable bullet in CLAUDE.md (Workflow section). MUST be invoked before retrying a `git commit` or `git push` that was blocked by .claude/hooks/preflight-claude-md.sh. Reads CLAUDE.md and the diff cold in fresh context, writes a structured verdict to .claude/.last-audit.json. The hook only allows the next retry when the verdict file is PASS and matches the current branch + HEAD.
tools: Read, Bash, Write
---

You are the CLAUDE.md compliance auditor for the boxlite3 repository.

The parent agent must tell you the exact git command they are about to retry
(e.g. `git commit -m "..."` or `git push origin <branch>`). Treat that as the
"target command" below.

## Procedure

1. Read `CLAUDE.md` from the repo root. Locate the `## Workflow` section.
2. Capture current repo state:
- `git branch --show-current`
- `git rev-parse HEAD`
3. Capture the diff that is about to land:
- If the target command starts with `git commit`: `git diff --cached`.
- If it starts with `git push`: `git diff origin/main...HEAD`.
4. For each Workflow phase (Understand / Research / Design / Implement / Test /
Verify / Cross-cutting):
- Identify which bullets are applicable to this diff. Skip ones that don't
apply (e.g. concurrency rules for a pure docs change).
- Judge PASS or FAIL against what the diff actually shows. Be skeptical:
missing tests, scope creep, undocumented new dependencies, secrets,
weakened assertions, comments that restate code, etc.
5. Write `.claude/.last-audit.json` with EXACTLY this shape (no extra fields):
```json
{
"branch": "<from step 2>",
"head": "<from step 2>",
"command_kind": "commit" | "push",
"verdict": "PASS" | "FAIL",
"findings": ["<phase>: <one-line description>", "..."]
}
```
On PASS, `findings` is an empty array.
6. Reply to the parent agent with the verdict and findings.

## Constraints

- Only judge — do not propose fixes, do not edit code, do not retry the git
command yourself.
- Do not skip phases. If a phase has no applicable bullets for this diff, say so
explicitly in your reply (not in findings).
- The hook reads only the JSON file; your chat reply is for the parent agent's
benefit. Both must agree.
126 changes: 126 additions & 0 deletions .claude/hooks/preflight-claude-md.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# PreToolUse hook: gate `git commit` / `git push` on a fresh verdict from the
# claude-md-auditor subagent (see .claude/agents/claude-md-auditor.md).
#
# The hook itself does not call any model; it only reads the verdict artifact
# the subagent writes at .claude/.last-audit.json and checks that the verdict
# is PASS, recent, and bound to the current branch + HEAD.
#
# Flow on a denied attempt:
# 1. Hook denies the git tool call.
# 2. Reason text instructs the parent agent to invoke the claude-md-auditor
# subagent via the Task tool, then retry the same git command.
# 3. Subagent writes .claude/.last-audit.json.
# 4. Parent retries -> hook reads the artifact and allows on PASS.
#
# Wired in .claude/settings.json under hooks.PreToolUse with matcher "Bash".
#
# Design notes
# ------------
# * Matcher scope: settings.json registers this hook on the broad `Bash`
# matcher, not a narrower `Bash:git*` pattern, because Claude Code's
# PreToolUse matchers are tool-name-only — there's no built-in way to filter
# on the bash command itself. The script does the actual filtering via the
# case match below and exits 0 immediately on non-target commands, so the
# per-invocation cost on unrelated bash calls is one jq parse + one regex.
#
# * One-shot consumption: the audit file is `rm -f`'d on the allow path
# (intentional, see end of script). This forces a fresh audit on every
# subsequent git commit/push — even at the same HEAD — so re-staged content
# between commits can't ride on the previous audit. The cost is that
# commit-then-push of the same HEAD must re-audit; the user has accepted
# this trade-off to avoid stale-audit-passes-new-content failure modes.
#
# Tests: bash .claude/hooks/preflight-claude-md.test.sh
set -euo pipefail

payload="$(cat)"
command="$(printf '%s' "$payload" | jq -r '.tool_input.command // ""')"

# Match when the command actually IS a `git commit` / `git push` invocation —
# at the start of the command OR at the start of any chain segment (after &&,
# ||, ;, |, &, $(, (, `). This catches the chained-command case
# (`cat foo && git commit ...`) that an anchor-only matcher misses, while still
# rejecting literal mentions of "git commit" inside string arguments (e.g.
# `echo "git commit"`), which don't sit at the start of a chain segment.
work="${command#"${command%%[![:space:]]*}"}"
if [[ "$work" =~ (^|[[:space:]]*(\&\&|\|\||;|\||\&|\$\(|\(|\`)[[:space:]]*)([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*git[[:space:]]+(commit|push)([[:space:]]|$) ]]; then
case "${BASH_REMATCH[4]}" in
commit) kind="commit" ;;
push) kind="push" ;;
*) exit 0 ;;
esac
else
exit 0
fi

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
project_dir="${CLAUDE_PROJECT_DIR:-$repo_root}"
branch="$(git -C "$repo_root" branch --show-current 2>/dev/null || echo '?')"
head="$(git -C "$repo_root" rev-parse HEAD 2>/dev/null || echo '?')"
audit_file="$project_dir/.claude/.last-audit.json"
max_age_seconds=600

deny() {
jq -nc --arg r "$1" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $r
}
}'
exit 0
}

invoke_instruction="Invoke the claude-md-auditor subagent now:
Task(subagent_type='claude-md-auditor',
description='CLAUDE.md audit',
prompt='Audit the pending \`${command}\` on branch ${branch}.')
The subagent will write its verdict to .claude/.last-audit.json. Retry the
same git command after it reports PASS."

if [[ ! -r "$audit_file" ]]; then
deny "No CLAUDE.md audit found for this change.

${invoke_instruction}"
fi

audit_branch="$(jq -r '.branch // ""' "$audit_file" 2>/dev/null || echo '')"
audit_head="$(jq -r '.head // ""' "$audit_file" 2>/dev/null || echo '')"
audit_kind="$(jq -r '.command_kind // ""' "$audit_file" 2>/dev/null || echo '')"
audit_verdict="$(jq -r '.verdict // ""' "$audit_file" 2>/dev/null || echo '')"

# File mtime as freshness signal — portable across BSD (stat -f %m) and GNU
# (stat -c %Y) without parsing self-reported timestamps.
audit_mtime="$(stat -f '%m' "$audit_file" 2>/dev/null || stat -c '%Y' "$audit_file" 2>/dev/null || echo 0)"
now_epoch="$(date +%s)"
age=$(( now_epoch - audit_mtime ))

if [[ "$audit_branch" != "$branch" ]] || \
[[ "$audit_head" != "$head" ]] || \
[[ "$audit_kind" != "$kind" ]] || \
(( age > max_age_seconds )); then
deny "Existing audit does not match current state:
audit.branch=${audit_branch} current=${branch}
audit.head=${audit_head} current=${head}
audit.command_kind=${audit_kind} current=${kind}
audit age: ${age}s (max ${max_age_seconds}s)

Re-audit is required.
${invoke_instruction}"
fi

if [[ "$audit_verdict" != "PASS" ]]; then
findings="$(jq -r '.findings[]? | " - " + .' "$audit_file" 2>/dev/null || echo '')"
deny "CLAUDE.md audit FAILED on branch '${branch}':

${findings}

Address each finding, then re-invoke claude-md-auditor before retrying \`${command}\`."
fi

# Verdict is PASS, recent, and matches current state — let the git command run.
# Consume the audit file so the next commit/push always re-audits, even if HEAD
# hasn't changed (e.g., user re-stages different content before the next commit).
rm -f "$audit_file"
exit 0
104 changes: 104 additions & 0 deletions .claude/hooks/preflight-claude-md.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Tests for .claude/hooks/preflight-claude-md.sh
#
# Covers the two areas CLAUDE.md flags for required tests on this change:
# 1. Command matcher (parsing + branching): direct invocation vs. chain
# segments vs. literal-string mentions inside arguments.
# 2. Gate logic (branching + boundary validation): missing / mismatched /
# stale / FAIL / consumed audit-file paths.
#
# Run with: bash .claude/hooks/preflight-claude-md.test.sh
# Exits non-zero on any failure.
set -uo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
HOOK="$REPO_ROOT/.claude/hooks/preflight-claude-md.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# Redirect the hook's audit-file lookup into TMP so tests don't touch the real
# .claude/.last-audit.json. The hook still uses git from the real repo for
# branch/HEAD detection — that's fine, we read the same values for assertions.
export CLAUDE_PROJECT_DIR="$TMP"
mkdir -p "$TMP/.claude"

BRANCH="$(git -C "$REPO_ROOT" branch --show-current)"
HEAD_SHA="$(git -C "$REPO_ROOT" rev-parse HEAD)"

pass=0
fail=0

run() {
local desc="$1" cmd="$2" expect="$3" out decision
out=$(printf '%s' "$cmd" | jq -Rs '{tool_input:{command:.}}' | "$HOOK")
if [[ -z "$out" ]]; then
decision="passthrough"
else
decision=$(printf '%s' "$out" | jq -r '.hookSpecificOutput.permissionDecision' 2>/dev/null || echo "parse_error")
fi
if [[ "$decision" == "$expect" ]]; then
pass=$((pass + 1))
printf ' PASS %s\n' "$desc"
else
fail=$((fail + 1))
printf ' FAIL %s (got=%s expected=%s)\n' "$desc" "$decision" "$expect"
fi
}

write_audit() {
local verdict="$1" findings_json="$2" kind="$3"
jq -nc --arg b "$BRANCH" --arg h "$HEAD_SHA" \
--arg v "$verdict" --arg k "$kind" \
--argjson f "$findings_json" \
'{branch:$b, head:$h, command_kind:$k, verdict:$v, findings:$f}' \
> "$TMP/.claude/.last-audit.json"
}

GC='git commit'
GP='git push'

echo "## Matcher: should pass through (not a git commit/push invocation)"
rm -f "$TMP/.claude/.last-audit.json"
run "ls" "ls" "passthrough"
run "echo with literal mention" "echo \"$GC\"" "passthrough"
run "grep with literal mention" "grep \"$GC\" file" "passthrough"
run "git status (different verb)" "git status" "passthrough"
run "git log (different verb)" "git log --oneline -5" "passthrough"

echo
echo "## Matcher: should gate (real git commit/push invocation)"
run "direct commit" "$GC -m wip" "deny"
run "direct push" "$GP origin main" "deny"
run "chained with &&" "cd x && $GC -m wip" "deny"
run "chained with ||" "true || $GC -m foo" "deny"
run "chained with ;" "echo done; $GC" "deny"
run "env var prefix" "FOO=bar $GC -m x" "deny"
run "command substitution" "out=\$($GC -m foo)" "deny"
run "push after &&" "cat x && $GP origin main" "deny"

echo
echo "## Gate logic: audit file states"
write_audit "PASS" "[]" "commit"
run "PASS verdict matches → allow" "$GC -m foo" "passthrough"
run "verdict consumed on allow" "$GC -m foo" "deny"

write_audit "FAIL" '["Test: missing"]' "commit"
run "FAIL verdict → deny" "$GC -m foo" "deny"

write_audit "PASS" "[]" "commit"
# Mutate head field to simulate a stale-by-HEAD audit
jq --arg h "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" '.head=$h' \
"$TMP/.claude/.last-audit.json" > "$TMP/.claude/x.json" \
&& mv "$TMP/.claude/x.json" "$TMP/.claude/.last-audit.json"
run "HEAD mismatch → deny" "$GC -m foo" "deny"

write_audit "PASS" "[]" "commit"
touch -t 202001010000 "$TMP/.claude/.last-audit.json"
run "stale mtime (>max_age) → deny" "$GC -m foo" "deny"

write_audit "PASS" "[]" "commit"
run "kind mismatch (commit vs push)" "$GP origin main" "deny"

echo
echo "RESULT: $pass passed, $fail failed"
exit $(( fail > 0 ? 1 : 0 ))
Loading
Loading