HookOutputBuilder is a static object that produces correctly-shaped JSON output for every hook event. Import it from @libar-dev/agent-harness-kit/types.
Source: src/utils/output-builder.ts
import { HookOutputBuilder } from '@libar-dev/agent-harness-kit/types';
import { outputJson } from '@libar-dev/agent-harness-kit/utils';
outputJson(HookOutputBuilder.permission('allow', 'Approved'));These methods work for any hook event.
success(message?: string): BaseHookOutputReturns a clean success response. With no message, sets suppressOutput: true (stdout hidden from transcript). With a message, includes it as systemMessage.
// Silent success (most common — just return without calling outputJson)
outputJson(HookOutputBuilder.success());
// Success with a visible message
outputJson(HookOutputBuilder.success('Hook ran successfully'));error(reason: string, stopExecution?: boolean): BaseHookOutputReturns an error response. If stopExecution is true, sets continue: false and stopReason.
outputJson(HookOutputBuilder.error('Something went wrong')); // non-blocking
outputJson(HookOutputBuilder.error('Cannot proceed', true)); // stops Claudepermission(
decision: 'allow' | 'deny' | 'ask' | 'defer',
reason: string,
options?: {
updatedInput?: Record<string, unknown>;
additionalContext?: string;
}
): PreToolUseOutputThe primary PreToolUse method. Produces hookSpecificOutput.permissionDecision.
| Decision | Effect |
|---|---|
'allow' |
Bypasses the permission system entirely — tool runs without user prompt |
'deny' |
Blocks the tool call — reason shown to Claude |
'ask' |
Prompts the user with your reason message |
'defer' |
Falls through to normal permission handling |
// Allow
outputJson(HookOutputBuilder.permission('allow', 'Safe command'));
// Deny
outputJson(HookOutputBuilder.permission('deny', 'rm -rf is not allowed here'));
// Ask (prompts user)
outputJson(HookOutputBuilder.permission('ask', 'This command looks risky. Proceed?'));
// Allow with modified input
outputJson(HookOutputBuilder.permission('allow', 'Redirected to safe path', {
updatedInput: { file_path: '/project/output/result.json' },
}));
// Allow with additional context for Claude
outputJson(HookOutputBuilder.permission('allow', 'Approved', {
additionalContext: 'Note: this command may take a while',
}));feedback(
reason: string,
additionalContext?: string,
updatedMCPToolOutput?: Record<string, unknown>
): PostToolUseOutputSends feedback to Claude after a tool executes. Sets decision: 'block' internally so the reason is shown to Claude. Use for formatter output, type-check results, or any observation Claude should act on.
updatedMCPToolOutput replaces the MCP tool's return value (MCP tools only).
outputJson(HookOutputBuilder.feedback('Formatted file with Prettier'));
outputJson(HookOutputBuilder.feedback('TypeScript errors found', tscStderr));
outputJson(HookOutputBuilder.feedback('MCP result overridden', undefined, { status: 'ok' }));allowPermission(options?: {
updatedInput?: Record<string, unknown>;
updatedPermissions?: PermissionUpdateEntry[];
}): PermissionRequestOutputAuto-approves a permission request. Pass updatedPermissions to apply "always allow" rules.
outputJson(HookOutputBuilder.allowPermission());
outputJson(HookOutputBuilder.allowPermission({
updatedInput: { file_path: '/safe/path.txt' },
}));denyPermission(options?: {
message?: string;
interrupt?: boolean;
}): PermissionRequestOutputAuto-denies a permission request. message is shown to Claude. interrupt: true stops Claude immediately.
outputJson(HookOutputBuilder.denyPermission({ message: 'Not allowed outside /project' }));
outputJson(HookOutputBuilder.denyPermission({ interrupt: true }));permissionRequestSetMode(
mode: PermissionMode,
destination?: 'session' | 'localSettings' | 'projectSettings' | 'userSettings'
): PermissionRequestOutputChanges the permission mode as part of allowing a request. Equivalent to the user selecting a mode in the permission dialog.
outputJson(HookOutputBuilder.permissionRequestSetMode('auto', 'session'));
outputJson(HookOutputBuilder.permissionRequestSetMode('acceptEdits', 'projectSettings'));PermissionMode values: 'default', 'plan', 'acceptEdits', 'auto', 'dontAsk', 'bypassPermissions'.
permissionDeniedRetry(retry: boolean): PermissionDeniedOutputFor PermissionDenied events — tells Claude whether it may retry the denied tool call.
outputJson(HookOutputBuilder.permissionDeniedRetry(true)); // allow retry
outputJson(HookOutputBuilder.permissionDeniedRetry(false)); // no retryblockPrompt(reason: string): UserPromptSubmitOutputBlocks the prompt from reaching Claude. The reason is shown to the user but is not added to context.
outputJson(HookOutputBuilder.blockPrompt('Prompt appears to contain an API key'));addContext(context: string): UserPromptSubmitOutputAdds a string to Claude's context before the prompt is processed.
outputJson(HookOutputBuilder.addContext(`Current date: ${new Date().toISOString().split('T')[0]}`));sessionTitle(title: string): UserPromptSubmitOutputSets the session title visible in the Claude Code UI.
outputJson(HookOutputBuilder.sessionTitle('Feature: user authentication'));sessionStartContext(context: string): SessionStartOutputInjects a string into the session's system context at startup.
const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
outputJson(HookOutputBuilder.sessionStartContext(`Branch: ${branch}`));subagentContext(context: string): SubagentStartOutputInjects context into a subagent's system prompt when it starts.
outputJson(HookOutputBuilder.subagentContext('This subagent operates in read-only mode'));subagentStopContext(reason: string): StopOutputSets decision: 'block' with a reason. Used for Stop and SubagentStop hooks to prevent stopping and provide guidance.
outputJson(HookOutputBuilder.subagentStopContext('Check the error log and fix the issue'));taskBlock(
reason: string,
hookEventName?: 'TaskCreated' | 'TaskCompleted'
): LifecycleStopOutputBlocks task creation or completion. Sets continue: false and stopReason.
outputJson(HookOutputBuilder.taskBlock('Task subject is too vague', 'TaskCreated'));
outputJson(HookOutputBuilder.taskBlock('Task was not completed correctly', 'TaskCompleted'));teammateStop(reason: string): LifecycleStopOutputBlocks a teammate from going idle (sets continue: false for TeammateIdle).
outputJson(HookOutputBuilder.teammateStop('Teammate has pending work'));batchBlock(reason: string): PostToolBatchOutputBlocks the agentic loop before the next model call after a tool batch. Sets decision: 'block' and injects reason as additionalContext.
outputJson(HookOutputBuilder.batchBlock('Batch produced unexpected file changes — review before continuing'));elicitation(
action: ElicitationAction,
content?: Record<string, unknown>,
hookEventName?: 'Elicitation' | 'ElicitationResult'
): ElicitationOutputProgrammatically responds to or overrides an MCP elicitation request.
action values: 'accept', 'decline', 'cancel'.
// Auto-accept a form elicitation
outputJson(HookOutputBuilder.elicitation('accept', { confirmed: true }));
// Decline an elicitation
outputJson(HookOutputBuilder.elicitation('decline'));
// Override an ElicitationResult
outputJson(HookOutputBuilder.elicitation('accept', { value: 'overridden' }, 'ElicitationResult'));watchPaths(paths: string[]): WatchPathsOutputReturns a new set of absolute paths for the file watcher to monitor. Used in CwdChanged and FileChanged hooks.
outputJson(HookOutputBuilder.watchPaths([
'/project/src',
'/project/config',
]));worktreePath(absolutePath: string): WorktreeCreateOutputReturns a custom absolute path for the new worktree. Any non-zero exit code overrides this and fails the creation.
import * as path from 'path';
import * as os from 'os';
outputJson(HookOutputBuilder.worktreePath(path.join(os.homedir(), 'worktrees', input.name)));stopFailureLog(systemMessage?: string): BaseHookOutputLogs a system message for observability on API failures. Delegates to success(systemMessage).
outputJson(HookOutputBuilder.stopFailureLog(`API error: ${input.error}`));