feat: file-read decision gate — block reads when observation history exists
Add a PreToolUse gate that blocks file reads on first attempt when rich observation history exists, presenting the timeline as feedback. Claude then decides: use get_observations() (skip read, save tokens) or re-read (allowed on second attempt). - FileReadGate: in-memory session-scoped gate with 4h TTL - POST /api/file-context/gate endpoint in worker - stderrMessage plumbing in hook-command for exit code 2 - file-context handler uses gate to block/allow reads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,7 +64,7 @@ function formatFileTimeline(observations: ObservationRow[], filePath: string): s
|
||||
});
|
||||
|
||||
const lines: string[] = [
|
||||
`Existing observations for this file \u2014 review via get_observations([IDs]) to avoid duplicates:`,
|
||||
`Read blocked: This file has prior observations. Use get_observations([IDs]) to load what you need. Re-read the file only if you need raw content not captured in observations:`,
|
||||
];
|
||||
|
||||
for (const [day, dayObservations] of sortedDays) {
|
||||
@@ -128,15 +128,28 @@ export const fileContextHandler: EventHandler = {
|
||||
return { continue: true, suppressOutput: true };
|
||||
}
|
||||
|
||||
const timeline = formatFileTimeline(data.observations, filePath);
|
||||
// Check the gate: has this file's timeline been shown in this session?
|
||||
const gateResponse = await workerHttpRequest('/api/file-context/gate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: input.sessionId, filePath: relativePath }),
|
||||
});
|
||||
|
||||
return {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'allow',
|
||||
additionalContext: timeline,
|
||||
},
|
||||
};
|
||||
if (gateResponse.ok) {
|
||||
const gateData = await gateResponse.json() as { firstAttempt: boolean };
|
||||
|
||||
if (gateData.firstAttempt) {
|
||||
// BLOCK: Show timeline, Claude decides whether to re-read or use get_observations()
|
||||
const timeline = formatFileTimeline(data.observations, filePath);
|
||||
return {
|
||||
exitCode: HOOK_EXIT_CODES.BLOCKING_ERROR,
|
||||
stderrMessage: timeline,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ALLOW: Second attempt or gate check failed — let the read proceed silently
|
||||
return { continue: true, suppressOutput: true };
|
||||
} catch (error) {
|
||||
logger.warn('HOOK', 'File context fetch error, skipping', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
||||
Reference in New Issue
Block a user