Claude Code hook handler for angelo.
Console entry point: angelo-claude-hook
Reads Claude Code hook input JSON from stdin, writes hook output JSON to stdout.
Each subcommand maps a Cursor prompt-hook to a Claude Code command-hook equivalent.
decide_pre_edit_context
decide_pre_edit_context(hook_input: dict) -> str | None
Deterministically pick the pre-edit nudge (or None to stay silent).
Keys purely on facts the hook can observe — the target path and whether it
exists on disk — never on an LLM's judgment of whether orientation happened:
- path unknown → the existing-file nudge (safe default)
- known non-code file →
None (config/docs/lockfiles: no symbol graph)
- known new (absent) file → the new-file note (absent anchor is expected)
- known existing file → the orient nudge
Source code in angelo_cli/claude_hooks.py
| def decide_pre_edit_context(hook_input: dict) -> str | None:
"""Deterministically pick the pre-edit nudge (or ``None`` to stay silent).
Keys purely on facts the hook can observe — the target path and whether it
exists on disk — never on an LLM's judgment of whether orientation happened:
- path unknown → the existing-file nudge (safe default)
- known non-code file → ``None`` (config/docs/lockfiles: no symbol graph)
- known new (absent) file → the new-file note (absent anchor is expected)
- known existing file → the orient nudge
"""
path = _resolve_edit_path(hook_input)
if path is None:
return PRE_EDIT_EXISTING_CONTEXT
if not _is_code_path(path):
return None
try:
exists = path.exists()
except OSError:
exists = True # fail toward nudging rather than silently skipping
return PRE_EDIT_EXISTING_CONTEXT if exists else PRE_EDIT_NEW_CONTEXT
|
decide_post_edit_context
decide_post_edit_context(hook_input: dict) -> str | None
Deterministically pick the post-edit tether reminder (or None).
Fires for code-file edits (new or existing — recording a tether applies to
both) and stays silent for non-code files. When the path is unknown it errs
toward reminding, since the reminder is advisory and cheap.
Source code in angelo_cli/claude_hooks.py
| def decide_post_edit_context(hook_input: dict) -> str | None:
"""Deterministically pick the post-edit tether reminder (or ``None``).
Fires for code-file edits (new or existing — recording a tether applies to
both) and stays silent for non-code files. When the path is unknown it errs
toward reminding, since the reminder is advisory and cheap.
"""
path = _resolve_edit_path(hook_input)
if path is not None and not _is_code_path(path):
return None
return POST_EDIT_TETHER_CONTEXT
|
handle_pre_edit
handle_pre_edit() -> None
PreToolUse (Claude): deterministically nudge the code-aware-editing reflex.
Claude Code's PreToolUse command hook can inject additionalContext without
blocking, so the decision is made in code (path + existence) rather than left to
a fast-LLM prompt — a new file's expected 'anchor absent' can never be misread
as 'not oriented'. Emits nothing for non-code files.
Source code in angelo_cli/claude_hooks.py
| def handle_pre_edit() -> None:
"""PreToolUse (Claude): deterministically nudge the code-aware-editing reflex.
Claude Code's PreToolUse command hook can inject ``additionalContext`` without
blocking, so the decision is made in code (path + existence) rather than left to
a fast-LLM prompt — a new file's expected 'anchor absent' can never be misread
as 'not oriented'. Emits nothing for non-code files.
"""
hook_input = _read_input()
context = decide_pre_edit_context(hook_input)
if context is None:
return
_emit({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": context,
}
})
|
handle_post_bash
handle_post_bash() -> None
After a Bash tool call: if it was a git commit/push, flush memory + zettel files.
Deterministic and silent — runs the same isolated, safe-push memory commit
and the no-push zettelkasten flush as the Cursor afterShellExecution hook.
Emits no context; never blocks.
Source code in angelo_cli/claude_hooks.py
| def handle_post_bash() -> None:
"""After a Bash tool call: if it was a git commit/push, flush memory + zettel files.
Deterministic and silent — runs the same isolated, safe-push memory commit
and the no-push zettelkasten flush as the Cursor afterShellExecution hook.
Emits no context; never blocks.
"""
hook_input = _read_input()
command = ""
tool_input = hook_input.get("tool_input")
if isinstance(tool_input, dict):
command = tool_input.get("command") or ""
if command and _GIT_COMMIT_RE.search(command):
try:
from angelo_cli import commit_dirty_memory, commit_dirty_zettel
commit_dirty_memory(quiet=True)
commit_dirty_zettel(quiet=True)
except Exception:
pass
|