Summary
Enable Express/Koa-style middleware composition so hooks can be built from small, reusable units.
Motivation
Complex hooks tend to grow into monolithic handlers that are hard to test and reuse. A composition model lets users combine small, focused functions into pipelines.
Proposed API
import { compose, deny } from "@dawkinsuke/hooks"
cc.on("PreToolUse", "Bash", compose(
logExecution, // log the invocation
rateLimiter(10), // 10 calls/min limit
guardDangerousCommands, // block dangerous commands
))
Design
Each middleware receives the input and a next function. Return HookOutput to short-circuit, or call next() to continue the chain:
import { HookOutput, deny } from "@dawkinsuke/hooks"
type Middleware<E extends HookEvent> = (
input: HookEventMap[E],
next: () => Promise<HookOutput | void>,
) => Promise<HookOutput | void>
function rateLimiter(max: number): Middleware<"PreToolUse"> {
return async (input, next) => {
const count = await store.get<number>("bash_count", 0)
if (count >= max) return deny("Rate limit exceeded")
await store.set("bash_count", count + 1)
return next()
}
}
function guardDangerousCommands(): Middleware<"PreToolUse"> {
return async (input, next) => {
if (input.tool_input.command?.match(/rm\s+-rf\s+\//)) {
return deny("Destructive command blocked")
}
return next()
}
}
Benefits
- Each middleware is independently testable
- Presets from the Built-in Hook Library are naturally middleware-shaped
HookOutput chaining works within middleware (deny("x").context("y"))
- Order is explicit and readable
- Early termination via
HookOutput return short-circuits the chain
tool_input is Record<string, any> — no casts needed in middleware
Summary
Enable Express/Koa-style middleware composition so hooks can be built from small, reusable units.
Motivation
Complex hooks tend to grow into monolithic handlers that are hard to test and reuse. A composition model lets users combine small, focused functions into pipelines.
Proposed API
Design
Each middleware receives the input and a
nextfunction. ReturnHookOutputto short-circuit, or callnext()to continue the chain:Benefits
HookOutputchaining works within middleware (deny("x").context("y"))HookOutputreturn short-circuits the chaintool_inputisRecord<string, any>— no casts needed in middleware