Hook Commands

Commands for running and managing hooks.

Commands for running validation hooks and managing hook execution.

han hook run

Run a validation hook from an installed plugin.

Usage

# New format: Run a plugin's hook
han hook run <plugin-name> <hook-name>

# With options
han hook run <plugin-name> <hook-name> --verbose

# Disable caching
han hook run <plugin-name> <hook-name> --no-cache

# Legacy format: Run custom command across directories
han hook run --dirs-with <file> -- <command>

Options

OptionDescription
--no-cacheDisable caching (caching is ON by default in v2.0.0+)
--only <directory>Only run in the specified directory, for targeted re-runs after a failure
--verboseShow full command output in real-time
--skip-depsSkip dependency checks, for recheck and retry scenarios
--session-id <id>Claude session ID used for event logging and cache tracking
--asyncEnable per-file ${HAN_FILES} substitution (PostToolUse) or session-file substitution (Stop)

--checkpoint-type <type> and --checkpoint-id <id> are still parsed and validated, but the hook runner ignores them. They are inert remnants of the removed checkpoint feature.

Legacy options:

OptionDescription
--dirs-with <file>Only run in directories containing the specified file
--test-dir <command>Only include directories where this command exits 0

Breaking Change (v2.0.0): Caching is enabled by default. Use --no-cache to disable it.

Caching Behavior

Caching is enabled by default (since v2.0.0). A hook run:

  1. Builds a manifest of the files matching the hook's if_changed patterns
  2. Skips the hook when nothing the current session touched appears in that manifest
  3. Otherwise compares each file's hash against the hash recorded at its last validation, and runs when any differ or a validated file was deleted
  4. Assumes changes and runs on any error, so a cache fault never silently skips validation

Pass --no-cache (or set HAN_NO_CACHE=1) to force a run.

Examples

# Run Bun tests (caching enabled by default)
han hook run bun test

# Run without caching
han hook run bun test --no-cache

# Run TypeScript type checking verbosely
han hook run typescript typecheck --verbose

# Run Biome lint in one directory only
han hook run biome lint --only packages/core

# Legacy: Run npm test in directories with package.json
han hook run --dirs-with package.json -- npm test

Plugin Hook Configuration

Hooks are defined in plugin han-plugin.yml files:

hooks:
  test:
    command: bun test --only-failures
    dirs_with: [bun.lock, bun.lockb]
    description: Run Bun tests
    if_changed: ["**/*.ts", "**/*.test.ts"]

When you run han hook run bun test, Han:

  1. Finds directories containing bun.lock or bun.lockb
  2. Checks whether files matching **/*.ts or **/*.test.ts have changed (caching is enabled by default)
  3. Runs bun test --only-failures in each directory
  4. Records the result and updates the file-validation cache

han hook list

List available hooks from installed plugins.

Usage

# List all available hooks
han hook list

# Filter by plugin
han hook list --plugin bun

# Filter by event type
han hook list --event Stop

# Show detailed information
han hook list --verbose

Options

OptionDescription
-e, --event <event>Filter by event type (for example Stop, PreToolUse)
-p, --plugin <name>Filter hooks by plugin name (substring match)
-v, --verboseShow additional details including source paths
--jsonOutput as JSON for scripting

Output

Available Hooks:

  bun:
    test - Run Bun tests
    build - Build the Bun project

  typescript:
    typecheck - Type-check TypeScript code for type errors

  biome:
    lint - Lint Biome code for issues and style violations

Examples

# List all hooks
han hook list

# List only Bun hooks
han hook list --plugin bun

# Get JSON output for scripting
han hook list --json | jq '.[] | select(.plugin == "bun")'

han hook explain

Show comprehensive information about configured hooks. By default it covers every hook Claude Code would fire, both Han plugin hooks and hooks defined in settings files.

The optional positional argument is an event type, not a plugin or hook name.

Usage

# All hooks, Han plugins plus Claude Code settings
han hook explain

# Only Stop hooks, from all sources
han hook explain Stop

# Only hooks contributed by Han plugins
han hook explain --han-only

# Combine both
han hook explain Stop --han-only

Options

OptionDescription
--han-onlyShow only Han plugin hooks, excluding settings hooks

han hook test

Run hooks with simulated Claude Code input and show their actual output. This executes the hooks, which is the point: it reproduces a hook failure exactly as Claude Code would trigger it.

As with explain, the optional positional argument is an event type.

Usage

# Test every hook
han hook test

# Test only SessionStart hooks
han hook test SessionStart

# Show the stdin payload sent to hooks
han hook test Stop --payload

# Only hooks whose command contains "han"
han hook test --command han

Options

OptionDescription
--payloadShow the stdin JSON payload sent to hooks
--command <substring>Filter to hooks whose command contains this string

han hook dispatch

Run every Han plugin hook registered for one event. This is what Claude Code invokes from a generated hooks.json.

Usage

han hook dispatch Stop
han hook dispatch PostToolUse --all

Options

OptionDescription
-a, --allInclude hooks defined in Claude Code settings, not just Han plugin hooks
--no-cacheDisable caching for the dispatched hooks

--no-checkpoints exports HAN_NO_CHECKPOINTS=1 to child hooks, which turns off session-scoped ${HAN_FILES} filtering so hooks check the whole tree. See session-scoped validation.

Other han hook subcommands

CommandPurpose
han hook contextPrint consolidated session context for SessionStart injection
han hook auto-detectAuto-install plugins from file changes, for PostToolUse
han hook auto-detect-promptAuto-install plugins from URLs and patterns in a prompt, for UserPromptSubmit
han hook wrap-subagent-contextPreToolUse helper that injects context into Agent and Skill tool prompts

Environment Variables

Hook execution respects these environment variables:

VariableDescription
HAN_DISABLE_HOOKSSet to 1 or true to disable all hooks
HAN_FORCE_HOOKSSet to 1 or true to run hooks even where they would otherwise be suppressed
HAN_HOOK_RUN_VERBOSESet to 1 or true to enable verbose output globally
HAN_NO_CACHESet to 1 or true to disable caching for this run
HAN_SESSION_IDSession ID used for event logging, cache tracking, and lock scoping
HAN_DEBUGSet to 1 or true for debug diagnostics on stderr
HAN_HOOK_ABSOLUTE_TIMEOUTHard ceiling on a single hook, in seconds (default: 300)
HAN_HOOK_PARALLELISMParallel hook slots (default: half the CPU count, minimum 1)
HAN_HOOK_ACQUIRE_TIMEOUTHow long to wait for a free slot, in milliseconds (default: 3600000)
HAN_HOOK_LOCK_TIMEOUTWhen a held lock is treated as stale, in milliseconds (default: 900000)
HAN_HOOK_NO_LOCKSet to 1 to disable hook resource locking entirely

Example

# Disable all hooks temporarily
export HAN_DISABLE_HOOKS=1
han hook run bun test  # Exits immediately without running

# Enable verbose output globally
export HAN_HOOK_RUN_VERBOSE=1
han hook run bun test  # Always shows full output

Integration with Claude Code

Hooks run automatically at lifecycle events when configured in plugin hooks.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "han hook run bun test" }
        ]
      }
    ],
    "SubagentStop": [
      {
        "hooks": [
          { "type": "command", "command": "han hook run typescript typecheck" }
        ]
      }
    ]
  }
}

Hook Lifecycle

Han accepts all 31 Claude Code hook events. The events below are the ones you will most often wire up from the CLI; see the full hook event reference for the complete list, current as of Claude Code 2.1.228.

HookWhen It FiresPurpose
SessionStartClaude Code session beginsInitialize session state
SubagentStartSubagent is spawnedInject context into the subagent
UserPromptSubmitUser submits a promptPre-process user input
PreToolUseBefore each tool callValidate tool usage
PermissionRequestPermission dialog appearsAudit or auto-approve permissions
PostToolUseAfter each tool callPost-process tool results
PostToolUseFailureTool execution failsError tracking, recovery
StopAgent about to respondValidate all session changes
SubagentStopSubagent completesValidate subagent changes
NotificationNotification eventCustom notification handling
PreCompactBefore context compactionSave state before compaction
SessionEndSession endsCleanup session state
ConfigChangeConfiguration modified (2.1.49+)Audit trails, config monitoring
TeammateIdleTeammate goes idle (2.1.33+)Team coordination
TaskCompletedTask completed (2.1.33+)Task tracking, workflows
DirectoryAddedDirectory added mid-session (2.1.219+)React to a newly registered working root
WorktreeCreateWorktree created (2.1.50+)Agent isolation tracking, custom VCS
WorktreeRemoveWorktree removed (2.1.50+)Cleanup automation

New Hook Events

PermissionRequest (~2.1.50+)

Fired when a permission dialog appears. Supports matcher on tool name. The input includes a permission_suggestions array. The hook can respond with behavior (allow/deny), updatedInput, updatedPermissions, message, or interrupt:

{
  "hook_event_name": "PermissionRequest",
  "session_id": "abc123",
  "cwd": "/project/path",
  "tool_name": "Bash",
  "permission_suggestions": [...]
}

Useful for automated permission policies, security auditing, and CI/CD environments.

PostToolUseFailure (~2.1.50+)

Fired when a tool execution fails. The input includes error string and is_interrupt boolean. The hook can return additionalContext to help Claude recover:

{
  "hook_event_name": "PostToolUseFailure",
  "session_id": "abc123",
  "cwd": "/project/path",
  "tool_name": "Bash",
  "error": "Command exited with code 1",
  "is_interrupt": false
}

Useful for error tracking, automatic recovery suggestions, and operational monitoring.

PreCompact (~2.1.50+)

Fired before context compaction. Supports matcher for manual vs auto compaction:

{
  "hook_event_name": "PreCompact",
  "session_id": "abc123",
  "cwd": "/project/path"
}

Useful for saving state, injecting critical context to preserve, or logging compaction events.

ConfigChange (2.1.49+)

Fired when Claude Code configuration is modified. The stdin payload includes the changed configuration keys:

{
  "hook_event_name": "ConfigChange",
  "session_id": "abc123",
  "cwd": "/project/path"
}

Useful for audit trails, configuration drift detection, and enforcing settings policies across teams.

TeammateIdle (2.1.33+)

Fired when a teammate agent goes idle between turns in multi-agent sessions:

{
  "hook_event_name": "TeammateIdle",
  "session_id": "abc123",
  "cwd": "/project/path"
}

Enables team coordination, load balancing, and monitoring agent activity.

TaskCompleted (2.1.33+)

Fired when a task is marked as completed via TaskUpdate:

{
  "hook_event_name": "TaskCompleted",
  "session_id": "abc123",
  "cwd": "/project/path"
}

Useful for task tracking dashboards, triggering follow-up workflows, and team notifications.

WorktreeCreate (2.1.50+)

Fired when a worktree is being created via --worktree flag or isolation: "worktree" in an agent definition. When a WorktreeCreate hook is configured, it replaces the default git worktree behavior, enabling support for non-git VCS systems (SVN, Perforce, Mercurial, etc.).

The hook receives a name slug and must print the absolute path to the created worktree directory on stdout. A non-zero exit code blocks worktree creation.

{
  "hook_event_name": "WorktreeCreate",
  "session_id": "abc123",
  "cwd": "/project/path",
  "name": "feature-auth"
}

Only type: "command" hooks are supported. No matcher support.

WorktreeRemove (2.1.50+)

Fired when a worktree is being removed (session exit or subagent completion). The hook receives the worktree_path that was created. WorktreeRemove hooks cannot block removal; failures are logged in debug mode only.

{
  "hook_event_name": "WorktreeRemove",
  "session_id": "abc123",
  "cwd": "/project/path",
  "worktree_path": "/project/.claude/worktrees/feature-auth"
}

Only type: "command" hooks are supported. No matcher support.

Stop/SubagentStop: last_assistant_message (2.1.47+)

The Stop and SubagentStop hook inputs now include a last_assistant_message field containing the final assistant message text. This allows hooks to inspect what the agent is about to respond with:

{
  "hook_event_name": "Stop",
  "session_id": "abc123",
  "cwd": "/project/path",
  "last_assistant_message": "I've completed the refactoring of the auth module..."
}

This is useful for content-aware validation, sentiment analysis, or logging the agent's final output.

How Hooks Narrow What They Check

Hooks do not re-check the whole tree on every run:

  • Caching skips a hook when nothing the current session touched matches its if_changed patterns, then compares file hashes against the last recorded validation
  • Session filtering hands a hook only the session's own modified files, for any command that uses ${HAN_FILES}

Earlier versions did this with explicit session and agent checkpoints captured at SessionStart and SubagentStart. That mechanism has been removed. The --checkpoint-type, --checkpoint-id, and --no-checkpoints flags survive as no-ops.

Learn More