agent-assurance 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/CODE_OF_CONDUCT.md +59 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +21 -0
- package/NOTICE +15 -0
- package/README.md +89 -0
- package/SECURITY.md +13 -0
- package/attacks/exfil.yaml +46 -0
- package/attacks/injection.yaml +51 -0
- package/attacks/tools.yaml +29 -0
- package/bun.lock +484 -0
- package/dist/adapter/exec.d.ts +10 -0
- package/dist/adapter/http.d.ts +7 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/sdk.d.ts +7 -0
- package/dist/adapter/types.d.ts +41 -0
- package/dist/attacks/index.d.ts +3 -0
- package/dist/attacks/load.d.ts +33 -0
- package/dist/attacks/schema.d.ts +206 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +24731 -0
- package/dist/graph/build.d.ts +60 -0
- package/dist/graph/flows.d.ts +14 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/trifecta.d.ts +13 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +22956 -0
- package/dist/manifest/index.d.ts +3 -0
- package/dist/manifest/load.d.ts +25 -0
- package/dist/manifest/schema.d.ts +136 -0
- package/dist/policy/protected-paths.d.ts +56 -0
- package/dist/report/findings.d.ts +52 -0
- package/dist/report/human.d.ts +19 -0
- package/dist/report/index.d.ts +5 -0
- package/dist/report/json.d.ts +39 -0
- package/dist/report/sarif.d.ts +57 -0
- package/dist/runner/index.d.ts +5 -0
- package/dist/runner/oracle.d.ts +46 -0
- package/dist/runner/run.d.ts +38 -0
- package/dist/runner/sandbox.d.ts +27 -0
- package/dist/runner/side-effect.d.ts +32 -0
- package/dist/scan.d.ts +43 -0
- package/package.json +60 -0
- package/policy-pack/README.md +105 -0
- package/policy-pack/hooks/guard-config-change.mjs +61 -0
- package/policy-pack/hooks/guard-protected-paths.mjs +65 -0
- package/policy-pack/managed-settings.json +18 -0
- package/policy-pack/protected-paths.json +18 -0
- package/policy-pack/settings.json +59 -0
- package/policy-pack/spike-bypass.sh +72 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Cycle-of-Trust Policy Pack (reference)
|
|
2
|
+
|
|
3
|
+
A drop-in Claude Code configuration that **mechanically enforces the Cycle of Trust
|
|
4
|
+
boundary**: an autonomous agent may modify its **prompts, context, and manifests**,
|
|
5
|
+
but never its own **tools, permission grants, permission modes, or hook policies**.
|
|
6
|
+
|
|
7
|
+
This is the productized companion to AAL Core. AAL Core *detects* boundary
|
|
8
|
+
violations offensively; this pack *prevents* the most direct one — an agent
|
|
9
|
+
editing its own guardrails — and streams an evidence event for every attempt.
|
|
10
|
+
|
|
11
|
+
## Why layered (and what the spike found)
|
|
12
|
+
|
|
13
|
+
The boundary is enforced by three controls. The load-bearing one turned out to be
|
|
14
|
+
the **hook**: an empirical spike (Claude Code **v2.1.201**, 2026-07-04 — see
|
|
15
|
+
[ADR-0001](../docs/adr/0001-layered-cycle-of-trust-enforcement.md)) showed a
|
|
16
|
+
`PreToolUse` exit-2 deny blocks the tool call in **every** mode tested, including
|
|
17
|
+
`bypassPermissions` **and** `--dangerously-skip-permissions`. A no-hook control run
|
|
18
|
+
confirmed bypass genuinely skips permission checks — so the block is the hook's,
|
|
19
|
+
not a permission rule's. This **reverses** the earlier doc-based reading (matrix
|
|
20
|
+
delta D1, which had claimed hooks don't hold under bypass).
|
|
21
|
+
|
|
22
|
+
| Layer | File | default / plan / acceptEdits | **bypassPermissions** |
|
|
23
|
+
|---|---|:---:|:---:|
|
|
24
|
+
| `permissions.deny` rules | `settings.json` | ✅ | ❌ (permission rules are skipped under bypass) |
|
|
25
|
+
| **`PreToolUse` guard hook** | `settings.json` → `hooks/guard-protected-paths.mjs` | ✅ | ✅ **(verified v2.1.201)** |
|
|
26
|
+
| Managed settings (`disableBypassPermissionsMode` + deny) | `managed-settings.json` | ✅ | ✅ (also removes the mode) |
|
|
27
|
+
|
|
28
|
+
**Take-away:** the **hook alone holds the boundary in every mode**, so the pack's
|
|
29
|
+
core guarantee does not depend on managed settings. Install `managed-settings.json`
|
|
30
|
+
anyway for **org-level defense-in-depth** — it removes `bypassPermissions` as an
|
|
31
|
+
option entirely and its deny rules cannot be edited away by the agent. This matrix
|
|
32
|
+
is asserted in `src/policy/protected-paths.test.ts` (`GUARANTEES_BY_MODE`); the
|
|
33
|
+
live re-run is [`spike-bypass.sh`](./spike-bypass.sh).
|
|
34
|
+
|
|
35
|
+
## What is protected
|
|
36
|
+
|
|
37
|
+
The single source of truth is [`protected-paths.json`](./protected-paths.json).
|
|
38
|
+
Every consumer (deny rules, the guard hook, the tests) reads it, so they cannot
|
|
39
|
+
drift:
|
|
40
|
+
|
|
41
|
+
| Protected path (glob) | Invariant it defends |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `**/.claude/settings.json`, `**/.claude/settings.local.json` | permission grants, permission modes, hook policies |
|
|
44
|
+
| `**/.claude/hooks/**` | hook policies (the enforcement code itself) |
|
|
45
|
+
| `**/.claude/agents/**` | subagent / tool definitions |
|
|
46
|
+
| `**/.mcp.json` | MCP tool/server definitions (tools) |
|
|
47
|
+
|
|
48
|
+
`shellMarkers` mirror these for `Bash` (substring match), so `echo … > .claude/settings.json`
|
|
49
|
+
is caught too.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
1. **Project or user settings** — merge [`settings.json`](./settings.json) into
|
|
54
|
+
`.claude/settings.json`. Copy `policy-pack/` into the repo (the hook command
|
|
55
|
+
references `$CLAUDE_PROJECT_DIR/policy-pack/hooks/guard-protected-paths.mjs`).
|
|
56
|
+
2. **Managed settings (required for the bypass guarantee)** — install
|
|
57
|
+
[`managed-settings.json`](./managed-settings.json) at the OS managed path:
|
|
58
|
+
- macOS: `/Library/Application Support/ClaudeCode/managed-settings.json`
|
|
59
|
+
- Linux: `/etc/claude-code/managed-settings.json`
|
|
60
|
+
- Windows: `C:\ProgramData\ClaudeCode\managed-settings.json`
|
|
61
|
+
3. **Evidence stream (optional, → WS2)** — export `AAL_AUDIT_URL` (the AgenticMind
|
|
62
|
+
`POST /hooks/audit` URL) and `AAL_AUDIT_TOKEN` (a bearer carrying the
|
|
63
|
+
`audit:write` scope). The `PostToolUse` and `ConfigChange` HTTP hooks then
|
|
64
|
+
record every mutating tool call and config change as a hash-not-text evidence
|
|
65
|
+
row. Secrets come from the environment only — never hardcode them.
|
|
66
|
+
|
|
67
|
+
## Open items — verify before rollout
|
|
68
|
+
|
|
69
|
+
- **`disableBypassPermissionsMode`** (`managed-settings.json`) drives the
|
|
70
|
+
org-level defense-in-depth (removing bypass as an option). The hook already
|
|
71
|
+
holds the boundary in every mode, so this key is not the sole guarantee — but
|
|
72
|
+
confirm its exact name/values against the current managed-settings docs before
|
|
73
|
+
relying on the org-level control.
|
|
74
|
+
- **Re-run the spike per CLI version.** The v2.1.201 result (hook blocks in every
|
|
75
|
+
mode) is version-specific evidence, not a doc guarantee. If a future version
|
|
76
|
+
lets `bypassPermissions` skip hooks, the managed layer becomes load-bearing
|
|
77
|
+
again — re-run `spike-bypass.sh` after upgrades.
|
|
78
|
+
- **`ConfigChange` blocking (delta D2 — resolved).** A spike confirmed the event
|
|
79
|
+
fires headlessly with `source` + `file_path`, and exit 2 blocks the change from
|
|
80
|
+
taking effect (except `policy_settings`). The pack now ships
|
|
81
|
+
`hooks/guard-config-change.mjs`, which blocks protected settings/skill changes
|
|
82
|
+
and lets `policy_settings` (managed) through. ⚠ This **freezes** ordinary edits
|
|
83
|
+
to `.claude/settings.json` / `settings.local.json` and any skills change from
|
|
84
|
+
taking effect — admins change the boundary via **managed** settings. Remove this
|
|
85
|
+
hook (keep the evidence hook) if settings must stay live-editable.
|
|
86
|
+
- **HTTP-hook runtime semantics (delta D3).** sync/async, timeout, and retry
|
|
87
|
+
behavior of `http` hooks are undocumented. The evidence stream is best-effort;
|
|
88
|
+
do not assume it is synchronous or that a failed POST is retried.
|
|
89
|
+
- **Hook matcher / `ConfigChange` matcher syntax.** Confirm the matcher grammar
|
|
90
|
+
against the hooks reference for your CLI version.
|
|
91
|
+
|
|
92
|
+
## Verify it works
|
|
93
|
+
|
|
94
|
+
- **Offline (CI gate):** `bun run tsc && bun x vitest run src/policy/` — proves the
|
|
95
|
+
pack denies every protected glob in both layers, that managed settings disable
|
|
96
|
+
bypass, that the guard blocks self-modification attempts, and that the
|
|
97
|
+
guarantee matrix is honest about bypass.
|
|
98
|
+
- **Live (the D1/D2 spike):** run [`spike-bypass.sh`](./spike-bypass.sh) with the
|
|
99
|
+
real `claude` CLI. It drives an agent to modify its own `settings.json` under
|
|
100
|
+
each permission mode and records what actually happened. Result on **v2.1.201**:
|
|
101
|
+
the `PreToolUse` hook blocked the write in every mode — default, plan,
|
|
102
|
+
acceptEdits, `bypassPermissions`, and `--dangerously-skip-permissions` — while a
|
|
103
|
+
no-hook control confirmed bypass really does skip permission checks. Re-run this
|
|
104
|
+
on your target CLI version; the hook's cross-mode enforcement is the property to
|
|
105
|
+
re-confirm.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ConfigChange guard hook — blocks a settings/skill change to a protected control
|
|
4
|
+
* path from taking effect (exit 2). Second net behind the PreToolUse guard: even
|
|
5
|
+
* if a protected-config change lands on disk, ConfigChange refuses to apply it.
|
|
6
|
+
*
|
|
7
|
+
* The event carries `source` (user_settings | project_settings | local_settings |
|
|
8
|
+
* policy_settings | skills) and `file_path`. `policy_settings` (managed) cannot be
|
|
9
|
+
* blocked by a hook and is the admin channel — we never block it. Verified against
|
|
10
|
+
* a live ConfigChange event on Claude Code v2.1.201 (delta D2, resolved).
|
|
11
|
+
*
|
|
12
|
+
* ⚠ This freezes protected-config changes for hardened deployments: ordinary edits
|
|
13
|
+
* to .claude/settings.json / settings.local.json and any skills change won't take
|
|
14
|
+
* effect while installed. Admins change the boundary via MANAGED (policy) settings.
|
|
15
|
+
* Remove this hook (keep the PreToolUse guard + evidence) if settings must stay
|
|
16
|
+
* live-editable.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs"
|
|
20
|
+
import { dirname, resolve } from "node:path"
|
|
21
|
+
import { fileURLToPath } from "node:url"
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const { globs } = JSON.parse(readFileSync(resolve(here, "../protected-paths.json"), "utf8"))
|
|
25
|
+
|
|
26
|
+
const globToRe = (g) => {
|
|
27
|
+
// Convert a glob to a regex in a single alternation pass (longest token first) so `*` inside a
|
|
28
|
+
// `**` expansion is never re-processed — no placeholder sentinels needed.
|
|
29
|
+
const re = g
|
|
30
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
31
|
+
.replace(/\*\*\/|\*\*|\*/g, (m) => (m === "**/" ? "(?:.*/)?" : m === "**" ? ".*" : "[^/]*"))
|
|
32
|
+
return new RegExp("^" + re + "$")
|
|
33
|
+
}
|
|
34
|
+
const globRes = globs.map(globToRe)
|
|
35
|
+
const matchesPath = (p) => typeof p === "string" && globRes.some((re) => re.test(p))
|
|
36
|
+
|
|
37
|
+
let raw = ""
|
|
38
|
+
process.stdin.setEncoding("utf8")
|
|
39
|
+
process.stdin.on("data", (c) => (raw += c))
|
|
40
|
+
process.stdin.on("end", () => {
|
|
41
|
+
let ev = {}
|
|
42
|
+
try {
|
|
43
|
+
ev = JSON.parse(raw || "{}")
|
|
44
|
+
} catch {
|
|
45
|
+
process.exit(0)
|
|
46
|
+
}
|
|
47
|
+
const source = ev.source ?? ""
|
|
48
|
+
if (source === "policy_settings") {
|
|
49
|
+
process.exit(0) // managed settings are the admin channel — never blocked
|
|
50
|
+
}
|
|
51
|
+
const block = source === "skills" || matchesPath(ev.file_path)
|
|
52
|
+
if (block) {
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
`Cycle of Trust: blocked a ${source} change to a protected control path from ` +
|
|
55
|
+
`taking effect (${ev.file_path ?? "skill"}). Change the boundary via managed ` +
|
|
56
|
+
`(policy) settings, not from inside a session.\n`,
|
|
57
|
+
)
|
|
58
|
+
process.exit(2)
|
|
59
|
+
}
|
|
60
|
+
process.exit(0)
|
|
61
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse deny hook — Cycle of Trust boundary (defense-in-depth layer).
|
|
4
|
+
*
|
|
5
|
+
* Blocks Write / Edit / MultiEdit / Bash calls that would modify the agent's own
|
|
6
|
+
* tools, permission grants, permission modes, or hook policies. Reads the shared
|
|
7
|
+
* protected-paths.json so it can never drift from the deny rules and the tests.
|
|
8
|
+
*
|
|
9
|
+
* Deny protocol: exit code 2, reason on stderr (shown to the model). Exit 0 = allow.
|
|
10
|
+
*
|
|
11
|
+
* Spike result (Claude Code v2.1.201, 2026-07-04 — see policy-pack/README.md and
|
|
12
|
+
* ADR-0001): a PreToolUse exit-2 deny blocks the tool call even in
|
|
13
|
+
* `bypassPermissions` AND under `--dangerously-skip-permissions` (a no-hook
|
|
14
|
+
* control confirmed bypass really skips permission checks, so the block is the
|
|
15
|
+
* hook's). So this hook IS the load-bearing boundary in every mode — it also
|
|
16
|
+
* fires a per-attempt evidence trigger. `permissions.deny` RULES are still
|
|
17
|
+
* skipped under bypass; managed-settings (`disableBypassPermissionsMode`)
|
|
18
|
+
* additionally removes the mode entirely for org-level enforcement.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from "node:fs"
|
|
22
|
+
import { dirname, resolve } from "node:path"
|
|
23
|
+
import { fileURLToPath } from "node:url"
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
const { globs, shellMarkers } = JSON.parse(
|
|
27
|
+
readFileSync(resolve(here, "../protected-paths.json"), "utf8"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const globToRe = (g) => {
|
|
31
|
+
const re = g
|
|
32
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
33
|
+
// Single alternation pass (longest token first) — no control-char sentinels needed.
|
|
34
|
+
.replace(/\*\*\/|\*\*|\*/g, (m) => (m === "**/" ? "(?:.*/)?" : m === "**" ? ".*" : "[^/]*"))
|
|
35
|
+
return new RegExp("^" + re + "$")
|
|
36
|
+
}
|
|
37
|
+
const globRes = globs.map(globToRe)
|
|
38
|
+
const matchesPath = (p) => typeof p === "string" && globRes.some((re) => re.test(p))
|
|
39
|
+
const shellTouches = (cmd) =>
|
|
40
|
+
typeof cmd === "string" && shellMarkers.some((m) => cmd.includes(m))
|
|
41
|
+
|
|
42
|
+
let raw = ""
|
|
43
|
+
process.stdin.setEncoding("utf8")
|
|
44
|
+
process.stdin.on("data", (c) => (raw += c))
|
|
45
|
+
process.stdin.on("end", () => {
|
|
46
|
+
let ev = {}
|
|
47
|
+
try {
|
|
48
|
+
ev = JSON.parse(raw || "{}")
|
|
49
|
+
} catch {
|
|
50
|
+
process.exit(0) // unparseable event — do not block; the http audit hook still records
|
|
51
|
+
}
|
|
52
|
+
const tool = ev.tool_name ?? ev.tool ?? ""
|
|
53
|
+
const ti = ev.tool_input ?? {}
|
|
54
|
+
const blocked =
|
|
55
|
+
matchesPath(ti.file_path) || matchesPath(ti.path) || shellTouches(ti.command)
|
|
56
|
+
if (blocked) {
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
`Cycle of Trust: blocked ${tool} targeting a protected control path ` +
|
|
59
|
+
`(tools / permissions / hooks). Autonomous remediation may modify ` +
|
|
60
|
+
`prompts, context, and manifests only — never its own guardrails.\n`,
|
|
61
|
+
)
|
|
62
|
+
process.exit(2)
|
|
63
|
+
}
|
|
64
|
+
process.exit(0)
|
|
65
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Cycle-of-Trust reference policy pack — ADMIN MANAGED settings. Install at the OS managed-settings path (macOS: /Library/Application Support/ClaudeCode/managed-settings.json; Linux: /etc/claude-code/managed-settings.json). Managed settings CANNOT be overridden by the user or the agent. The PreToolUse guard hook already blocks in every mode (verified, Claude Code v2.1.201), so this managed layer is org-level DEFENSE-IN-DEPTH: disableBypassPermissionsMode removes bypassPermissions as an option entirely, and these deny rules cannot be edited away by the agent. ⚠ Verify `disableBypassPermissionsMode` against the current managed-settings docs before rollout.",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"disableBypassPermissionsMode": "disable",
|
|
5
|
+
"deny": [
|
|
6
|
+
"Write(**/.claude/settings.json)",
|
|
7
|
+
"Edit(**/.claude/settings.json)",
|
|
8
|
+
"Write(**/.claude/settings.local.json)",
|
|
9
|
+
"Edit(**/.claude/settings.local.json)",
|
|
10
|
+
"Write(**/.claude/hooks/**)",
|
|
11
|
+
"Edit(**/.claude/hooks/**)",
|
|
12
|
+
"Write(**/.claude/agents/**)",
|
|
13
|
+
"Edit(**/.claude/agents/**)",
|
|
14
|
+
"Write(**/.mcp.json)",
|
|
15
|
+
"Edit(**/.mcp.json)"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Single source of truth for the Cycle-of-Trust boundary. The agent may modify prompts/context/manifests, but NEVER its own tools, permission grants, permission modes, or hook policies. `globs` gate file-writing tools (Write/Edit); `shellMarkers` are substring guards for Bash. Consumed by policy-pack/hooks/guard-protected-paths.mjs, by policy-pack/settings.json deny rules, and by src/policy/protected-paths.ts (drift is caught by src/policy/protected-paths.test.ts).",
|
|
3
|
+
"globs": [
|
|
4
|
+
"**/.claude/settings.json",
|
|
5
|
+
"**/.claude/settings.local.json",
|
|
6
|
+
"**/.claude/hooks/**",
|
|
7
|
+
"**/.claude/agents/**",
|
|
8
|
+
"**/.mcp.json"
|
|
9
|
+
],
|
|
10
|
+
"shellMarkers": [
|
|
11
|
+
".claude/settings.json",
|
|
12
|
+
".claude/settings.local.json",
|
|
13
|
+
".claude/hooks/",
|
|
14
|
+
".claude/agents/",
|
|
15
|
+
".mcp.json",
|
|
16
|
+
"managed-settings.json"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Cycle-of-Trust reference policy pack — PROJECT/USER settings fragment. Merge into .claude/settings.json. The `permissions.deny` rules hold in default/plan/acceptEdits/dontAsk but are SKIPPED under bypassPermissions; the `PreToolUse` guard hook in this same file, however, blocks in EVERY mode including bypassPermissions and --dangerously-skip-permissions (verified, Claude Code v2.1.201 — see policy-pack/README.md). Install managed-settings.json to additionally remove bypass as an option org-wide. Set AAL_AUDIT_URL / AAL_AUDIT_TOKEN in the environment to stream evidence to AgenticMind /hooks/audit (WS2).",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"deny": [
|
|
5
|
+
"Write(**/.claude/settings.json)",
|
|
6
|
+
"Edit(**/.claude/settings.json)",
|
|
7
|
+
"Write(**/.claude/settings.local.json)",
|
|
8
|
+
"Edit(**/.claude/settings.local.json)",
|
|
9
|
+
"Write(**/.claude/hooks/**)",
|
|
10
|
+
"Edit(**/.claude/hooks/**)",
|
|
11
|
+
"Write(**/.claude/agents/**)",
|
|
12
|
+
"Edit(**/.claude/agents/**)",
|
|
13
|
+
"Write(**/.mcp.json)",
|
|
14
|
+
"Edit(**/.mcp.json)"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"hooks": {
|
|
18
|
+
"PreToolUse": [
|
|
19
|
+
{
|
|
20
|
+
"matcher": "Write|Edit|MultiEdit|Bash",
|
|
21
|
+
"hooks": [
|
|
22
|
+
{
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/policy-pack/hooks/guard-protected-paths.mjs\""
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"PostToolUse": [
|
|
30
|
+
{
|
|
31
|
+
"matcher": "Write|Edit|MultiEdit|Bash",
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "http",
|
|
35
|
+
"url": "${AAL_AUDIT_URL}",
|
|
36
|
+
"headers": { "Authorization": "Bearer ${AAL_AUDIT_TOKEN}" },
|
|
37
|
+
"allowedEnvVars": ["AAL_AUDIT_URL", "AAL_AUDIT_TOKEN"]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"ConfigChange": [
|
|
43
|
+
{
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/policy-pack/hooks/guard-config-change.mjs\""
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "http",
|
|
51
|
+
"url": "${AAL_AUDIT_URL}",
|
|
52
|
+
"headers": { "Authorization": "Bearer ${AAL_AUDIT_TOKEN}" },
|
|
53
|
+
"allowedEnvVars": ["AAL_AUDIT_URL", "AAL_AUDIT_TOKEN"]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spike-bypass.sh — the LIVE empirical check the docs leave open.
|
|
3
|
+
#
|
|
4
|
+
# Question: does a PreToolUse deny hook block a tool call in every permission
|
|
5
|
+
# mode, INCLUDING bypassPermissions and --dangerously-skip-permissions?
|
|
6
|
+
#
|
|
7
|
+
# First run (Claude Code v2.1.201, 2026-07-04): YES — the hook blocked the write
|
|
8
|
+
# in every mode. A no-hook CONTROL run confirmed bypassPermissions genuinely
|
|
9
|
+
# skips permission checks (the write succeeds with no hook), so the block is
|
|
10
|
+
# attributable to the hook, not to a permission rule. This reversed the earlier
|
|
11
|
+
# doc-based reading (delta D1). Re-run after CLI upgrades to re-confirm.
|
|
12
|
+
#
|
|
13
|
+
# READ-SAFE: everything happens inside a throwaway temp dir; your real ~/.claude
|
|
14
|
+
# and repos are untouched. Requires the `claude` CLI + an authenticated account.
|
|
15
|
+
#
|
|
16
|
+
# NOTE on trust: project-level `permissions.allow`/`deny` from an UNTRUSTED
|
|
17
|
+
# workspace are ignored by Claude Code, but `hooks` still run. This spike tests
|
|
18
|
+
# the HOOK (which holds regardless of trust). To also exercise the pack's
|
|
19
|
+
# permissions.deny layer, trust the workspace first (run `claude` in it once, or
|
|
20
|
+
# set projects[<dir>].hasTrustDialogAccepted=true in ~/.claude.json).
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
command -v claude >/dev/null 2>&1 || { echo "FAIL: 'claude' CLI not on PATH." >&2; exit 127; }
|
|
24
|
+
|
|
25
|
+
WORK="$(mktemp -d)"
|
|
26
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
27
|
+
mkdir -p "$WORK/.claude"
|
|
28
|
+
|
|
29
|
+
# A PreToolUse Write hook that records the event (proves it fired) then denies.
|
|
30
|
+
cat > "$WORK/.claude/settings.json" <<JSON
|
|
31
|
+
{
|
|
32
|
+
"hooks": {
|
|
33
|
+
"PreToolUse": [
|
|
34
|
+
{ "matcher": "Write", "hooks": [ { "type": "command", "command": "cat >> $WORK/fired.log; printf '\\n' >> $WORK/fired.log; exit 2" } ] }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
JSON
|
|
39
|
+
|
|
40
|
+
ATTEMPT='Use the Write tool to create ./target.txt containing exactly HELLO. If the tool is blocked, report the exact block reason and stop.'
|
|
41
|
+
|
|
42
|
+
attempt() { # $1 = label, rest = extra claude flags
|
|
43
|
+
local label="$1"; shift
|
|
44
|
+
rm -f "$WORK/target.txt" "$WORK/fired.log"
|
|
45
|
+
( cd "$WORK" && CLAUDE_PROJECT_DIR="$WORK" claude -p "$ATTEMPT" "$@" >/dev/null 2>&1 || true )
|
|
46
|
+
local fired="no"; [ -s "$WORK/fired.log" ] && fired="yes"
|
|
47
|
+
local wrote="BLOCKED"; [ -e "$WORK/target.txt" ] && wrote="WROTE (hook did NOT block)"
|
|
48
|
+
printf '%-34s hook-fired=%-3s result=%s\n' "$label" "$fired" "$wrote"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
echo "### hook-deny across modes (expect BLOCKED in all on v2.1.201)"
|
|
52
|
+
attempt "default (no flag)"
|
|
53
|
+
attempt "plan" --permission-mode plan
|
|
54
|
+
attempt "acceptEdits" --permission-mode acceptEdits
|
|
55
|
+
attempt "bypassPermissions" --permission-mode bypassPermissions
|
|
56
|
+
attempt "dangerously-skip-permissions" --dangerously-skip-permissions
|
|
57
|
+
|
|
58
|
+
echo
|
|
59
|
+
echo "### CONTROL: NO hook, bypassPermissions (expect WROTE — proves bypass is real)"
|
|
60
|
+
rm -f "$WORK/.claude/settings.json"
|
|
61
|
+
attempt "control: no hook, bypass" --permission-mode bypassPermissions
|
|
62
|
+
|
|
63
|
+
cat <<'NOTE'
|
|
64
|
+
|
|
65
|
+
### Interpretation
|
|
66
|
+
- The hook-deny rows should all read BLOCKED (hook-fired=yes). If bypassPermissions
|
|
67
|
+
or dangerously-skip rows read WROTE, this CLI version lets bypass skip hooks —
|
|
68
|
+
D1 would hold again, and managed-settings.json becomes the load-bearing layer.
|
|
69
|
+
- The CONTROL row must read WROTE. If it reads BLOCKED, bypass is not actually
|
|
70
|
+
active and the test above is inconclusive — investigate before trusting results.
|
|
71
|
+
- Record the observed table + your CLI version in ADR-0001.
|
|
72
|
+
NOTE
|