Skip to content

Commit 91de234

Browse files
grypezclaude
andcommitted
feat(caprock): capture deny-list snapshot and provision_match events
Two observability additions to the caprock hook, prerequisite to the audit CLI but independently useful for transcript-driven inspection. - Reads `permissions.deny` from each watched settings file (in addition to the existing allow list) and captures it on the session state as `settingsDenySnapshot`, so the at-start view of authority is complete. - Records a `provision_match` event in the session log whenever a PreToolUse routing succeeds, naming the matched provisions. This makes "which provision authorized this tool use" inspectable from the event stream rather than only from the in-vat ledger. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent da670ca commit 91de234

3 files changed

Lines changed: 51 additions & 10 deletions

File tree

packages/caprock/bin/hook.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
appendEvent,
4848
readEvents,
4949
readSettingsAllowList,
50+
readSettingsDenyList,
5051
caprockOutputPath,
5152
} from '../src/session.ts';
5253
import type {
@@ -394,20 +395,30 @@ async function getOrInitSession(payload: {
394395
rootKref,
395396
subclusterId,
396397
startedAt: now(),
397-
settingsSnapshot: snapshot,
398+
settingsSnapshot: snapshot.allow,
399+
settingsDenySnapshot: snapshot.deny,
398400
};
399401
await saveSessionState(session_id, state);
400402
return state;
401403
}
402404

403405
/**
404-
* Collect the current union of all watched settings allow-lists.
406+
* Collect the current union of all watched settings permission lists.
405407
*
406-
* @returns The deduplicated list of permission allow entries.
408+
* @returns Deduplicated allow and deny entry lists.
407409
*/
408-
async function collectSettingsSnapshot(): Promise<string[]> {
409-
const lists = await Promise.all(SETTINGS_PATHS.map(readSettingsAllowList));
410-
return [...new Set(lists.flat())];
410+
async function collectSettingsSnapshot(): Promise<{
411+
allow: string[];
412+
deny: string[];
413+
}> {
414+
const [allowLists, denyLists] = await Promise.all([
415+
Promise.all(SETTINGS_PATHS.map(readSettingsAllowList)),
416+
Promise.all(SETTINGS_PATHS.map(readSettingsDenyList)),
417+
]);
418+
return {
419+
allow: [...new Set(allowLists.flat())],
420+
deny: [...new Set(denyLists.flat())],
421+
};
411422
}
412423

413424
// ─── Hook output helpers ──────────────────────────────────────────────────────
@@ -484,7 +495,8 @@ async function onSessionStart(payload: SessionStartPayload): Promise<void> {
484495
rootKref,
485496
subclusterId,
486497
startedAt: now(),
487-
settingsSnapshot: snapshot,
498+
settingsSnapshot: snapshot.allow,
499+
settingsDenySnapshot: snapshot.deny,
488500
};
489501
await saveSessionState(session_id, state);
490502

@@ -495,7 +507,7 @@ async function onSessionStart(payload: SessionStartPayload): Promise<void> {
495507
kernelSessionId,
496508
rootKref,
497509
transcriptPath: transcript_path,
498-
settingsAllowCount: snapshot.length,
510+
settingsAllowCount: snapshot.allow.length,
499511
});
500512

501513
const connectCmd = `ocap modal ${kernelSessionId}`;
@@ -506,7 +518,7 @@ async function onSessionStart(payload: SessionStartPayload): Promise<void> {
506518
process.stdout.write(
507519
`${JSON.stringify({
508520
output:
509-
`[caprock] tracking authority → ${caprockFile} (${snapshot.length} rules in allowlist)\n` +
521+
`[caprock] tracking authority → ${caprockFile} (${snapshot.allow.length} rules in allowlist)\n` +
510522
`[caprock] TUI: run \`ocap tui\` (session appears automatically) or \`${connectCmd}\` to connect directly`,
511523
})}\n`,
512524
);
@@ -566,6 +578,14 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
566578
const provisions = matches.filter(
567579
(matched): matched is Provision => matched !== null,
568580
);
581+
await appendEvent(session_id, {
582+
t: now(),
583+
event: 'provision_match',
584+
sessionId: session_id,
585+
toolName: tool_name,
586+
inputSha: sha,
587+
provisions,
588+
});
569589
await recordProvisioned(
570590
SOCKET_PATH,
571591
state.kernelSessionId,

packages/caprock/src/session.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,25 @@ export async function readSettingsAllowList(
113113
}
114114
}
115115

116+
/**
117+
* Read the permissions.deny list from a Claude Code settings file.
118+
*
119+
* @param settingsPath - Absolute path to the settings JSON file.
120+
* @returns The deny list, or an empty array if the file is absent or unreadable.
121+
*/
122+
export async function readSettingsDenyList(
123+
settingsPath: string,
124+
): Promise<string[]> {
125+
try {
126+
const raw = JSON.parse(await readFile(settingsPath, 'utf8')) as {
127+
permissions?: { deny?: string[] };
128+
};
129+
return raw.permissions?.deny ?? [];
130+
} catch {
131+
return [];
132+
}
133+
}
134+
116135
/**
117136
* Derive the colocated caprock output path from the session transcript path.
118137
* e.g. `~/.claude/projects/.../<uuid>.jsonl` → `<uuid>.caprock.jsonl`

packages/caprock/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type SessionState = {
88
subclusterId: string;
99
startedAt: string;
1010
settingsSnapshot: string[];
11+
settingsDenySnapshot?: string[];
1112
};
1213

1314
export type CaprockEventKind =
@@ -20,7 +21,8 @@ export type CaprockEventKind =
2021
| 'rule_grant'
2122
| 'tui_accept'
2223
| 'tui_reject'
23-
| 'connect_hint';
24+
| 'connect_hint'
25+
| 'provision_match';
2426

2527
export type CaprockEvent = {
2628
t: string;

0 commit comments

Comments
 (0)