claudewheel v0.24.2 /claudewheel.project_hooks
Edit
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

python
def has_hooks(self) -> bool

True when at least one settings file contributes a hooks section.

#fingerprint

python
def fingerprint(self) -> str

Stable 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

python
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

python
def _entry_lines(event: str, entry: Any) -> list[str]

Render one hooks entry ({matcher?, hooks: [...]}) to display lines.

#_load_hooks_section

python
def _load_hooks_section(path: Path) -> Any

Return 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

python
def read_project_hooks(directory: str) -> ProjectHooks

Read 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

python
def target_directory(selections: dict[str, str | None]) -> str

Resolve 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.

Search