Lifecycle hooks as a subprocess-based observer pattern — extensible without modifying core code.
sequenceDiagram
participant CC as Claude Code
participant HE as Hook Executor
participant H1 as Hook Script 1
participant H2 as Hook Script 2
CC->>HE: pre_tool_use event
HE->>H1: spawn subprocess
HE->>H2: spawn subprocess
H1-->>HE: exit 0 (allow)
H2-->>HE: exit 1 (deny)
HE-->>CC: deniedMermaid diagram definition
The hooks system is an Observer pattern where the observers are shell scripts, not code objects. This has a powerful property: hooks can be written in any language, don't need to import anything, and can't crash Claude Code if they fail.
Hooks are isolated by design — each runs in its own subprocess with no shared memory. A hook that takes 30 seconds doesn't block the UI. A hook that crashes doesn't affect other hooks.
`pre_tool_use` hooks can deny tool execution by exiting with a non-zero code. This gives users a way to implement custom permission policies — e.g., a hook that blocks all bash commands during a deployment window.
Hook executor showing subprocess spawning, environment variable injection, and deny semantics.
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
/**
* Hooks are user-defined shell commands that can be executed at various points
* in Claude Code's lifecycle.
*/
import { basename } from 'path'
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
import { pathExists } from './file.js'
import { wrapSpawn } from './ShellCommand.js'
import { TaskOutput } from './task/TaskOutput.js'
import { getCwd } from './cwd.js'
import { randomUUID } from 'crypto'
import { formatShellPrefixCommand } from './bash/shellPrefix.js'
import {
getHookEnvFilePath,
invalidateSessionEnvCache,
} from './sessionEnvironment.js'
import { subprocessEnv } from './subprocessEnv.js'
import { getPlatform } from './platform.js'
import { findGitBashPath, windowsPathToPosixPath } from './windowsPaths.js'
import { getCachedPowerShellPath } from './shell/powershellDetection.js'
import { DEFAULT_HOOK_SHELL } from './shell/shellProvider.js'
import { buildPowerShellArgs } from './shell/powershellProvider.js'
import {
loadPluginOptions,
substituteUserConfigVariables,
} from './plugins/pluginOptionsStorage.js'
import { getPluginDataDir } from './plugins/pluginDirectories.js'
import {
getSessionId,
getProjectRoot,
getIsNonInteractiveSession,
getRegisteredHooks,
getStatsStore,
addToTurnHookDuration,
getOriginalCwd,
getMainThreadAgentType,
} from '../bootstrap/state.js'
import { checkHasTrustDialogAccepted } from './config.js'
import {
getHooksConfigFromSnapshot,
shouldAllowManagedHooksOnly,
shouldDisableAllHooksIncludingManaged,
} from './hooks/hooksConfigSnapshot.js'
import {
getTranscriptPathForSession,
getAgentTranscriptPath,
} from './sessionStorage.js'
import type { AgentId } from '../types/ids.js'
import {Ask anything about Observer Pattern
Powered by Groq · Enter to send, Shift+Enter for newline