fix: preserve accumulated modifiedInput and common fields on deny/ask from exit code paths

When a hook returns exit code 2 (deny) or 1 (ask), previously accumulated
modifiedInput and common fields from earlier allow hooks were discarded.
Now all exit paths (exit code and JSON) include accumulated state.
This commit is contained in:
kilhyeonjun
2026-04-10 10:40:12 +09:00
parent 5d8bd99f8f
commit fe1cfd885c
2 changed files with 36 additions and 0 deletions
@@ -216,5 +216,37 @@ describe("executePreToolUseHooks", () => {
expect(result.suppressOutput).toBe(true)
expect(result.systemMessage).toBe("Budget warning: approaching limit")
})
it("#when first hook allows with modifiedInput and second hook denies #then deny includes accumulated modifiedInput", async () => {
let callCount = 0
dispatchSpy.mockImplementation(async () => {
callCount++
if (callCount === 1) {
return {
exitCode: 0,
stdout: JSON.stringify({
decision: "allow",
hookSpecificOutput: {
permissionDecision: "allow",
updatedInput: { file_path: "/tmp/modified.md" },
},
}),
stderr: "",
}
}
return { exitCode: 2, stdout: "", stderr: "BUDGET EXCEEDED" }
})
const config = createConfig([
{ matcher: "*", hooks: [{ type: "command", command: "node modifier.mjs" }] },
{ matcher: "Edit|Write", hooks: [{ type: "command", command: "bash budget-guard.sh" }] },
])
const result = await executePreToolUseHooks(createContext(), config)
expect(callCount).toBe(2)
expect(result.decision).toBe("deny")
expect(result.modifiedInput).toEqual({ file_path: "/tmp/modified.md" })
})
})
})
@@ -100,10 +100,12 @@ export async function executePreToolUseHooks(
return {
decision: "deny",
reason: result.stderr || result.stdout || "Hook blocked the operation",
modifiedInput: accumulatedModifiedInput,
elapsedMs: Date.now() - startTime,
hookName: firstHookName,
toolName: transformedToolName,
inputLines,
...accumulatedCommonFields,
}
}
@@ -111,10 +113,12 @@ export async function executePreToolUseHooks(
return {
decision: "ask",
reason: result.stderr || result.stdout,
modifiedInput: accumulatedModifiedInput,
elapsedMs: Date.now() - startTime,
hookName: firstHookName,
toolName: transformedToolName,
inputLines,
...accumulatedCommonFields,
}
}