On this page
Load project .claude/settings*.json hooks, compute a SHA-256 fingerprint for change detection, and produce a listing for approval.
#claudewheel.project_hooks
#claudewheel.project_hooks
Read and fingerprint a target project's Claude Code hooks.
A project can contribute its own Claude Code hooks via .claude/settings.json and .claude/settings.local.json (each file's top-level hooks section). Those hooks run arbitrary commands, so claudewheel must show them for explicit approval before a launch trusts them -- and re-prompt whenever they change.
This module is the reader half: it loads both config files, extracts their hooks sections, computes a canonical fingerprint over the combined content (so a launch can detect first-sighting and change), and produces a flattened, human-readable listing for the approval page. Malformed JSON is a hard error carrying the offending filename (it aborts the launch -- never a silent skip).
#MalformedProjectHooksError
A project settings file could not be parsed as JSON.
Carries the bare :attr:filename (e.g. settings.local.json) so callers can name the broken file in an actionable abort message.
#ProjectHooks
The combined hooks a project contributes, keyed by source filename.
sources maps each contributing file's basename (e.g. settings.json) to that file's hooks section. Files that are absent, that lack a hooks key, or whose hooks is empty do NOT appear -- so has_hooks is a clean "does this project contribute anything" signal.
#has_hooks
def has_hooks(self) -> boolTrue when at least one settings file contributes a hooks section.
#fingerprint
def fingerprint(self) -> strStable sha256 hex over the canonical JSON of the combined hooks.
Canonical serialization (sorted keys, compact separators) makes the fingerprint independent of key ordering and whitespace: identical hooks content always yields the same fingerprint, and any change -- including moving a hook between the two settings files -- yields a different one.
#listing_lines
def listing_lines(self) -> list[str]Flatten the hooks into human-readable lines for the approval page.
One line per command: the event name, the matcher (when present), and the command string. The Claude Code hooks schema is tolerated loosely -- missing or extra fields never raise; only what exists is shown. An entry with a matcher but no commands still contributes a line so the reviewer sees every matcher. Ordering is deterministic (sorted by filename, then event) so the listing is stable across launches.
#_entry_lines
def _entry_lines(event: str, entry: Any) -> list[str]Render one hooks entry ({matcher?, hooks: [...]}) to display lines.
#_load_hooks_section
def _load_hooks_section(path: Path) -> AnyReturn the hooks section of path, or None if absent/empty.
An absent file or a file without a (non-empty) hooks key contributes nothing. Unparseable JSON raises :class:MalformedProjectHooksError naming the file -- this is a hard error, never a silent skip.
#read_project_hooks
def read_project_hooks(directory: str) -> ProjectHooksRead the combined Claude Code hooks a project under directory declares.
Reads <directory>/.claude/settings.json and settings.local.json, extracting each file's hooks section. Absent files and absent/empty hooks keys are simply skipped. Malformed JSON in either file raises :class:MalformedProjectHooksError carrying the offending filename.
#target_directory
def target_directory(selections: dict[str, str | None]) -> strResolve the launch target directory from selections (mirrors launch).
Uses the directory selection (~ expanded) when set, else the current working directory -- the same rule the launch config resolver applies.