Skip to content

angelo_cli.claude_hooks

angelo_cli.claude_hooks

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