Merge pull request #2771 from MoerAI/fix/bash-file-read-guard
fix(hooks): add bash-file-read-guard to warn agents against cat/head/tail (fixes #2096)
This commit is contained in:
@@ -47,6 +47,7 @@ export const HookNameSchema = z.enum([
|
|||||||
"tasks-todowrite-disabler",
|
"tasks-todowrite-disabler",
|
||||||
"runtime-fallback",
|
"runtime-fallback",
|
||||||
"write-existing-file-guard",
|
"write-existing-file-guard",
|
||||||
|
"bash-file-read-guard",
|
||||||
"anthropic-effort",
|
"anthropic-effort",
|
||||||
"hashline-read-enhancer",
|
"hashline-read-enhancer",
|
||||||
"read-image-resizer",
|
"read-image-resizer",
|
||||||
|
|||||||
44
src/hooks/bash-file-read-guard.ts
Normal file
44
src/hooks/bash-file-read-guard.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Hooks } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
import { log } from "../shared"
|
||||||
|
|
||||||
|
const WARNING_MESSAGE = "Prefer the Read tool over `cat`/`head`/`tail` for reading file contents. The Read tool provides line numbers and hash anchors for precise editing."
|
||||||
|
|
||||||
|
const FILE_READ_PATTERNS = [
|
||||||
|
/^\s*cat\s+(?!-)[^\s|&;]+\s*$/,
|
||||||
|
/^\s*head\s+(-n\s+\d+\s+)?(?!-)[^\s|&;]+\s*$/,
|
||||||
|
/^\s*tail\s+(-n\s+\d+\s+)?(?!-)[^\s|&;]+\s*$/,
|
||||||
|
]
|
||||||
|
|
||||||
|
function isSimpleFileReadCommand(command: string): boolean {
|
||||||
|
return FILE_READ_PATTERNS.some((pattern) => pattern.test(command))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBashFileReadGuardHook(): Hooks {
|
||||||
|
return {
|
||||||
|
"tool.execute.before": async (
|
||||||
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
|
output: { args: Record<string, unknown>; message?: string },
|
||||||
|
): Promise<void> => {
|
||||||
|
if (input.tool.toLowerCase() !== "bash") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = output.args.command
|
||||||
|
if (typeof command !== "string") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSimpleFileReadCommand(command)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output.message = WARNING_MESSAGE
|
||||||
|
|
||||||
|
log("[bash-file-read-guard] warned on bash file read command", {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
command,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ export { createPreemptiveCompactionHook } from "./preemptive-compaction";
|
|||||||
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
||||||
export { createRuntimeFallbackHook, type RuntimeFallbackHook, type RuntimeFallbackOptions } from "./runtime-fallback";
|
export { createRuntimeFallbackHook, type RuntimeFallbackHook, type RuntimeFallbackOptions } from "./runtime-fallback";
|
||||||
export { createWriteExistingFileGuardHook } from "./write-existing-file-guard";
|
export { createWriteExistingFileGuardHook } from "./write-existing-file-guard";
|
||||||
|
export { createBashFileReadGuardHook } from "./bash-file-read-guard";
|
||||||
export { createHashlineReadEnhancerHook } from "./hashline-read-enhancer";
|
export { createHashlineReadEnhancerHook } from "./hashline-read-enhancer";
|
||||||
export { createJsonErrorRecoveryHook, JSON_ERROR_TOOL_EXCLUDE_LIST, JSON_ERROR_PATTERNS, JSON_ERROR_REMINDER } from "./json-error-recovery";
|
export { createJsonErrorRecoveryHook, JSON_ERROR_TOOL_EXCLUDE_LIST, JSON_ERROR_PATTERNS, JSON_ERROR_REMINDER } from "./json-error-recovery";
|
||||||
export { createReadImageResizerHook } from "./read-image-resizer"
|
export { createReadImageResizerHook } from "./read-image-resizer"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
createRulesInjectorHook,
|
createRulesInjectorHook,
|
||||||
createTasksTodowriteDisablerHook,
|
createTasksTodowriteDisablerHook,
|
||||||
createWriteExistingFileGuardHook,
|
createWriteExistingFileGuardHook,
|
||||||
|
createBashFileReadGuardHook,
|
||||||
createHashlineReadEnhancerHook,
|
createHashlineReadEnhancerHook,
|
||||||
createReadImageResizerHook,
|
createReadImageResizerHook,
|
||||||
createJsonErrorRecoveryHook,
|
createJsonErrorRecoveryHook,
|
||||||
@@ -34,6 +35,7 @@ export type ToolGuardHooks = {
|
|||||||
rulesInjector: ReturnType<typeof createRulesInjectorHook> | null
|
rulesInjector: ReturnType<typeof createRulesInjectorHook> | null
|
||||||
tasksTodowriteDisabler: ReturnType<typeof createTasksTodowriteDisablerHook> | null
|
tasksTodowriteDisabler: ReturnType<typeof createTasksTodowriteDisablerHook> | null
|
||||||
writeExistingFileGuard: ReturnType<typeof createWriteExistingFileGuardHook> | null
|
writeExistingFileGuard: ReturnType<typeof createWriteExistingFileGuardHook> | null
|
||||||
|
bashFileReadGuard: ReturnType<typeof createBashFileReadGuardHook> | null
|
||||||
hashlineReadEnhancer: ReturnType<typeof createHashlineReadEnhancerHook> | null
|
hashlineReadEnhancer: ReturnType<typeof createHashlineReadEnhancerHook> | null
|
||||||
jsonErrorRecovery: ReturnType<typeof createJsonErrorRecoveryHook> | null
|
jsonErrorRecovery: ReturnType<typeof createJsonErrorRecoveryHook> | null
|
||||||
readImageResizer: ReturnType<typeof createReadImageResizerHook> | null
|
readImageResizer: ReturnType<typeof createReadImageResizerHook> | null
|
||||||
@@ -103,6 +105,10 @@ export function createToolGuardHooks(args: {
|
|||||||
? safeHook("write-existing-file-guard", () => createWriteExistingFileGuardHook(ctx))
|
? safeHook("write-existing-file-guard", () => createWriteExistingFileGuardHook(ctx))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
const bashFileReadGuard = isHookEnabled("bash-file-read-guard")
|
||||||
|
? safeHook("bash-file-read-guard", () => createBashFileReadGuardHook())
|
||||||
|
: null
|
||||||
|
|
||||||
const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer")
|
const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer")
|
||||||
? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? false } }))
|
? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? false } }))
|
||||||
: null
|
: null
|
||||||
@@ -132,6 +138,7 @@ export function createToolGuardHooks(args: {
|
|||||||
rulesInjector,
|
rulesInjector,
|
||||||
tasksTodowriteDisabler,
|
tasksTodowriteDisabler,
|
||||||
writeExistingFileGuard,
|
writeExistingFileGuard,
|
||||||
|
bashFileReadGuard,
|
||||||
hashlineReadEnhancer,
|
hashlineReadEnhancer,
|
||||||
jsonErrorRecovery,
|
jsonErrorRecovery,
|
||||||
readImageResizer,
|
readImageResizer,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function createToolExecuteBeforeHandler(args: {
|
|||||||
await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output)
|
await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.claudeCodeHooks?.["tool.execute.before"]?.(input, output)
|
await hooks.claudeCodeHooks?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.nonInteractiveEnv?.["tool.execute.before"]?.(input, output)
|
await hooks.nonInteractiveEnv?.["tool.execute.before"]?.(input, output)
|
||||||
|
await hooks.bashFileReadGuard?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.commentChecker?.["tool.execute.before"]?.(input, output)
|
await hooks.commentChecker?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.directoryAgentsInjector?.["tool.execute.before"]?.(input, output)
|
await hooks.directoryAgentsInjector?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.directoryReadmeInjector?.["tool.execute.before"]?.(input, output)
|
await hooks.directoryReadmeInjector?.["tool.execute.before"]?.(input, output)
|
||||||
|
|||||||
Reference in New Issue
Block a user