In the previous chapter, Event-Based Configuration Registry, we built the "Rulebook." We learned how to map specific events (like tool_finish) to a list of rules.
But we left one big question unanswered: What specific actions can we perform?
In Chapter 1, we simply ran a command. But what if you want to:
This chapter introduces Polymorphic Hook Definitions. Itβs a fancy name for a simple concept: A Universal Adapter.
Imagine you are building a "Deployment" workflow. When you finish writing code, you want three things to happen automatically:
If we only supported shell commands, steps 2 and 3 would be very hard to implement. We need a system that can handle different "shapes" of data depending on the task.
Think of your wall outlet. You can plug in:
The outlet doesn't care what the device is, as long as the plug fits. In our system, the "plug" is a JSON object, and the "label" that tells us what the device does is the type field.
Let's look at how we define these three different actions using our polymorphic schema.
This is the worker. It executes scripts on your machine.
{
// The label that identifies this as a Shell Command
"type": "command",
// Specific settings for commands
"command": "npm run format",
"timeout": 30 // Stop after 30 seconds
}
Explanation: When the system sees type: "command", it knows to look for a command string and executes it in the terminal.
This is the messenger. It talks to the outside world.
{
// The label that identifies this as a Network Request
"type": "http",
// Specific settings for HTTP
"url": "https://api.myapp.com/logs",
"headers": { "Authorization": "Bearer $MY_TOKEN" }
}
Explanation: When the system sees type: "http", it stops looking for shell commands. Instead, it expects a url. It sends the event data there automatically.
This is the thinker. It asks an LLM a question.
{
// The label that identifies this as an AI Prompt
"type": "prompt",
// Specific settings for AI
"prompt": "Review the code in $ARGUMENTS for security issues.",
"model": "claude-3-5-sonnet"
}
Explanation: When the system sees type: "prompt", it knows it needs to talk to an AI model. It sends the prompt text to the LLM and streams the response back to you.
How does the code know which logic to run? It uses a pattern called a Discriminated Union.
Here is how the system processes a generic hook object:
type property first.
Let's look at hooks.ts to see how this is built using Zod.
First, we define the individual shapes (schemas) for each type.
// hooks.ts (Simplified)
const BashCommandHookSchema = z.object({
type: z.literal('command'), // This must be "command"
command: z.string(),
timeout: z.number().optional(),
});
const HttpHookSchema = z.object({
type: z.literal('http'), // This must be "http"
url: z.string().url(),
});
Explanation:
z.literal('command'): This is the magic. It enforces that if you use the BashCommandHookSchema, the type field must be exactly the string "command".Now, we combine them into one master schema. This is the "Polymorphic" part.
// hooks.ts
export const HookCommandSchema = lazySchema(() => {
const { BashCommandHookSchema, HttpHookSchema, /* others */ } = buildHookSchemas()
return z.discriminatedUnion('type', [
BashCommandHookSchema,
HttpHookSchema,
// PromptHookSchema,
// AgentHookSchema
])
})
Explanation:
z.discriminatedUnion: This tells Zod, "I have a list of possible objects. They are all different, but they all share one common field called 'type'."'type' first. If it finds "http", it only checks against HttpHookSchema and ignores the Command schema.If we didn't use this approach, we might have a messy object like this:
// The WRONG way (Bad design)
{
"command": "npm run test", // Optional?
"url": "..." // Optional?
"prompt": "..." // Optional?
}
In the "Wrong Way," you could accidentally provide both a command and a url, confusing the system.
With Polymorphic Definitions, the structure is strict. If type is "command", you are not allowed to have a url property. It keeps the configuration clean and error-free.
In this chapter, we learned:
type field acts as a discriminator (or label) to tell the system how to handle the data.discriminatedUnion ensures strict validationβyou can't mix up settings meant for a URL with settings meant for a shell command.Now that we know how to define these hooks, let's see how the most common one actually works.
Next Chapter: Shell & System Integration
Generated by Code IQ