Hooks
Hooks are TypeScript modules that intercept and modify internal operations. Unlike extensions (which use event-based APIs), hooks use a simpler input/output transformation model for specific injection points.
Note: Most customization should use extensions instead. Hooks are lower-level and intended for specific use cases where you need to transform data at well-defined points.
When to Use Hooks vs Extensions
| Use Hooks When | Use Extensions When |
|---|---|
| Transforming chat parameters | Listening to lifecycle events |
| Modifying tool arguments/results | Registering custom tools |
| Adding custom headers to requests | Adding slash commands |
| Transforming system prompts | Building interactive UIs |
| Modifying shell environment | Stateful tool implementations |
Hook Locations
Hooks are loaded from:
- Global:
~/.indusagi/agent/hooks/*.ts - Project:
.indusagi/hooks/*.ts - Settings:
hooksarray insettings.json - CLI:
--hook <path>flag
Hook File Format
A hook file exports a factory function that receives context and returns hook handlers:
import type { HookFactory } from "indusagi-coding-agent";
export default ((ctx) => {
console.log(`Hooks loading in: ${ctx.cwd}`);
return {
// Hook handlers go here
"tool.execute.before": async (input, output) => {
// Modify tool arguments before execution
},
};
}) satisfies HookFactory;
HookInitContext
The factory receives an initialization context:
interface HookInitContext {
cwd: string; // Current working directory
agentDir: string; // Agent config directory
events: EventBus; // Shared event bus
exec: (command: string, args: string[], options?: ExecOptions) => Promise<ExecResult>;
}
Available Hooks
chat.params
Modify streaming parameters before calling the LLM.
"chat.params": async (input, output) => {
// input: { sessionId, model, systemPrompt }
// output: { options: SimpleStreamOptions }
// Add custom timeout
output.options.timeout = 60000;
// Enable caching
output.options.caching = true;
}
chat.headers
Add custom headers to LLM requests.
"chat.headers": async (input, output) => {
// input: { sessionId, model, systemPrompt }
// output: { headers: Record<string, string> }
// Add custom headers
output.headers["x-custom-header"] = "value";
output.headers["x-request-id"] = input.sessionId;
}
chat.message
Transform user messages before sending to LLM.
"chat.message": async (input, output) => {
// input: { sessionId, model? }
// output: { content: (TextContent | ImageContent)[] }
// Prepend context to message
output.content = [
{ type: "text", text: "Additional context:\n" },
...output.content,
];
}
tool.execute.before
Modify tool arguments before execution.
"tool.execute.before": async (input, output) => {
// input: { tool, sessionId, callId }
// output: { args: Record<string, unknown> }
if (input.tool === "bash") {
// Log all bash commands
console.log(`Bash: ${output.args.command}`);
// Inject environment variable
output.args.command = `export CUSTOM_VAR=1 && ${output.args.command}`;
}
if (input.tool === "write") {
// Block writes to sensitive paths
if (String(output.args.path).includes(".env")) {
throw new Error("Cannot write to .env files");
}
}
}
tool.execute.after
Transform tool results after execution.
"tool.execute.after": async (input, output) => {
// input: { tool, sessionId, callId, args }
// output: { content, details, isError }
if (input.tool === "bash" && !output.isError) {
// Truncate long output
const text = output.content[0];
if (text?.type === "text" && text.text.length > 10000) {
output.content = [{
type: "text",
text: text.text.slice(0, 10000) + "\n... (truncated)",
}];
}
}
}
tool.definition
Modify tool descriptions and parameters.
"tool.definition": async (input, output) => {
// input: { toolId }
// output: { description, parameters }
if (input.toolId === "bash") {
// Add usage examples to description
output.description += "\n\nExamples:\n- List files: `ls -la`\n- Find text: `grep -r 'pattern' .`";
}
}
shell.env
Inject environment variables into shell sessions.
"shell.env": async (input, output) => {
// input: { cwd }
// output: { env: Record<string, string> }
// Add custom environment
output.env["MY_PROJECT_ROOT"] = input.cwd;
output.env["NODE_ENV"] = "development";
// Load from .env file
const dotenv = await import("dotenv");
const config = dotenv.config({ path: `${input.cwd}/.env` });
if (config.parsed) {
Object.assign(output.env, config.parsed);
}
}
command.execute.before
Transform extension command arguments.
"command.execute.before": async (input, output) => {
// input: { sessionId, command, args }
// output: { args: string }
if (input.command === "mycommand") {
// Preprocess arguments
output.args = output.args.toUpperCase();
}
}
Experimental Hooks
These hooks are experimental and may change:
experimental.chat.system.transform
Transform the system prompt.
"experimental.chat.system.transform": async (input, output) => {
// input: { sessionId, model }
// output: { system: string }
// Append custom instructions
output.system += "\n\nAdditional instructions:\n- Be concise\n- Focus on code quality";
}
experimental.chat.messages.transform
Transform the message history.
"experimental.chat.messages.transform": async (input, output) => {
// input: { sessionId }
// output: { messages: AgentMessage[] }
// Filter out old messages
const cutoff = Date.now() - 3600000; // 1 hour
output.messages = output.messages.filter(
(m) => m.timestamp > cutoff
);
}
experimental.session.compacting
Customize session compaction.
"experimental.session.compacting": async (input, output) => {
// input: { sessionId }
// output: { context: string[], prompt?: string }
// Custom compaction prompt
output.prompt = "Summarize this conversation focusing on:\n- Decisions made\n- Files modified\n- Pending tasks";
}
experimental.text.complete
Transform auto-complete suggestions.
"experimental.text.complete": async (input, output) => {
// input: { sessionId }
// output: { text: string }
// Post-process completion
output.text = output.text.trim();
}
Error Handling
Hook errors are logged but don't crash the session:
"tool.execute.before": async (input, output) => {
throw new Error("This error is logged, session continues");
}
To abort an operation, modify the output appropriately:
"tool.execute.before": async (input, output) => {
// Block by throwing with specific message
if (forbiddenOperation(output.args)) {
throw new Error("Operation blocked by security policy");
}
}
Complete Example
// ~/.indusagi/agent/hooks/audit-logger.ts
import type { HookFactory } from "indusagi-coding-agent";
import * as fs from "fs";
import * as path from "path";
export default ((ctx) => {
const logFile = path.join(ctx.agentDir, "audit.log");
function log(entry: object) {
const line = JSON.stringify({ ...entry, timestamp: new Date().toISOString() }) + "\n";
fs.appendFileSync(logFile, line);
}
return {
"tool.execute.before": async (input, output) => {
log({
event: "tool_start",
tool: input.tool,
callId: input.callId,
args: output.args,
});
},
"tool.execute.after": async (input, output) => {
log({
event: "tool_end",
tool: input.tool,
callId: input.callId,
isError: output.isError,
});
},
"shell.env": async (input, output) => {
log({
event: "shell_start",
cwd: input.cwd,
});
},
};
}) satisfies HookFactory;
Loading Hooks
Via Settings
{
"hooks": [
"~/.indusagi/agent/hooks/audit-logger.ts",
".indusagi/hooks/project-hooks.ts"
]
}
Via CLI
indusagi --hook ./my-hooks.ts
Programmatically
import { loadHooks, discoverHooksInDir, DefaultResourceLoader } from "indusagi-coding-agent";
// Discover hooks in a directory
const hookPaths = discoverHooksInDir("./hooks");
// Load hooks
const result = await loadHooks(hookPaths, process.cwd());
if (result.errors.length > 0) {
console.error("Hook errors:", result.errors);
}
Related
- Extensions - Higher-level customization API
- SDK - Programmatic usage
- Settings - Configuration options
