fix: accumulate modifiedInput and common fields from allow hooks
When a hook returns 'allow' with updatedInput or common fields (suppressOutput, systemMessage, etc.), these values were silently dropped. Now they are accumulated across hooks and included in the final result, matching Claude Code's behavior where allow hooks can still modify tool input and set metadata.
This commit is contained in:
@@ -171,5 +171,50 @@ describe("executePreToolUseHooks", () => {
|
||||
|
||||
expect(callCount).toBe(2)
|
||||
})
|
||||
|
||||
it("#when hook returns allow with updatedInput #then modifiedInput is included in final result", async () => {
|
||||
dispatchSpy.mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
decision: "allow",
|
||||
hookSpecificOutput: {
|
||||
permissionDecision: "allow",
|
||||
updatedInput: { file_path: "/tmp/modified.md" },
|
||||
},
|
||||
}),
|
||||
stderr: "",
|
||||
})
|
||||
|
||||
const config = createConfig([
|
||||
{ matcher: "Write", hooks: [{ type: "command", command: "bash modifier.sh" }] },
|
||||
])
|
||||
|
||||
const result = await executePreToolUseHooks(createContext(), config)
|
||||
|
||||
expect(result.decision).toBe("allow")
|
||||
expect(result.modifiedInput).toEqual({ file_path: "/tmp/modified.md" })
|
||||
})
|
||||
|
||||
it("#when hook returns allow with common fields #then fields are included in final result", async () => {
|
||||
dispatchSpy.mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
decision: "allow",
|
||||
suppressOutput: true,
|
||||
systemMessage: "Budget warning: approaching limit",
|
||||
}),
|
||||
stderr: "",
|
||||
})
|
||||
|
||||
const config = createConfig([
|
||||
{ matcher: "Write", hooks: [{ type: "command", command: "bash checker.sh" }] },
|
||||
])
|
||||
|
||||
const result = await executePreToolUseHooks(createContext(), config)
|
||||
|
||||
expect(result.decision).toBe("allow")
|
||||
expect(result.suppressOutput).toBe(true)
|
||||
expect(result.systemMessage).toBe("Budget warning: approaching limit")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,6 +73,13 @@ export async function executePreToolUseHooks(
|
||||
const startTime = Date.now()
|
||||
let firstHookName: string | undefined
|
||||
const inputLines = buildInputLines(ctx.toolInput)
|
||||
let accumulatedModifiedInput: Record<string, unknown> | undefined
|
||||
let accumulatedCommonFields: {
|
||||
continue?: boolean
|
||||
stopReason?: string
|
||||
suppressOutput?: boolean
|
||||
systemMessage?: string
|
||||
} = {}
|
||||
|
||||
for (const matcher of matchers) {
|
||||
if (!matcher.hooks || matcher.hooks.length === 0) continue
|
||||
@@ -147,27 +154,36 @@ export async function executePreToolUseHooks(
|
||||
return {
|
||||
decision,
|
||||
reason,
|
||||
modifiedInput,
|
||||
modifiedInput: modifiedInput ?? accumulatedModifiedInput,
|
||||
elapsedMs: Date.now() - startTime,
|
||||
hookName: firstHookName,
|
||||
toolName: transformedToolName,
|
||||
inputLines,
|
||||
continue: output.continue,
|
||||
stopReason: output.stopReason,
|
||||
suppressOutput: output.suppressOutput,
|
||||
systemMessage: output.systemMessage,
|
||||
continue: output.continue ?? accumulatedCommonFields.continue,
|
||||
stopReason: output.stopReason ?? accumulatedCommonFields.stopReason,
|
||||
suppressOutput: output.suppressOutput ?? accumulatedCommonFields.suppressOutput,
|
||||
systemMessage: output.systemMessage ?? accumulatedCommonFields.systemMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// "allow" — apply modifiedInput but continue processing remaining hooks
|
||||
// "allow" — accumulate modifiedInput and common fields, continue to next hook
|
||||
if (modifiedInput) {
|
||||
accumulatedModifiedInput = { ...accumulatedModifiedInput, ...modifiedInput }
|
||||
Object.assign(stdinData.tool_input, objectToSnakeCase(modifiedInput))
|
||||
}
|
||||
if (output.continue !== undefined) accumulatedCommonFields.continue = output.continue
|
||||
if (output.stopReason !== undefined) accumulatedCommonFields.stopReason = output.stopReason
|
||||
if (output.suppressOutput !== undefined) accumulatedCommonFields.suppressOutput = output.suppressOutput
|
||||
if (output.systemMessage !== undefined) accumulatedCommonFields.systemMessage = output.systemMessage
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { decision: "allow" }
|
||||
return {
|
||||
decision: "allow" as const,
|
||||
...(accumulatedModifiedInput ? { modifiedInput: accumulatedModifiedInput } : {}),
|
||||
...(Object.keys(accumulatedCommonFields).length > 0 ? accumulatedCommonFields : {}),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user