Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions engine/hooks/unverified-tag-ledger/claude.hook.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@
]
}
]
},
"subagent_stop": {
"inherit": false,
"reason": "the ledger and its next-prompt reminder belong to the session owner; a subagent shares the parent's session id and would log its own tags against the parent's ledger, and it has no next user prompt to be reminded at"
}
}
74 changes: 74 additions & 0 deletions engine/hooks/unverified-tag-ledger/install_claude_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Idempotently merge unverified-tag-ledger's Claude Code hooks into
~/.claude/settings.json: the Stop hook (records tags and refuses a turn that
tagged without checking) and the UserPromptSubmit hook (re-surfaces claims
earlier turns deferred and never settled).

Safe to rerun on every `install.sh`: each hook type is identified by whether
any of its entries' "command" mentions that hook's own marker script,
replaces just that hook type's unverified-tag-ledger entry with the current fragment
file's content, and leaves every other key in settings.json (model, theme,
other hooks, other hook types, ...) untouched. That means editing either
fragment file and rerunning install.sh converges cleanly instead of
appending a duplicate entry each time.
"""
import json
import os

HERE = os.path.dirname(os.path.abspath(__file__))
SETTINGS_PATH = os.path.expanduser("~/.claude/settings.json")

# (hook type in settings.json, marker script identifying "our" entry, fragment file)
HOOK_SPECS = [
("Stop", "claude_stop_check.py", os.path.join(HERE, "claude.hook.json")),
("UserPromptSubmit", "claude_prompt_reminder.py", os.path.join(HERE, "claude.prompt.hook.json")),
]


def _is_ours(entry, marker):
return any(marker in h.get("command", "") for h in entry.get("hooks", []))


def merge_hook(settings, hook_type, marker, fragment):
"""Pure: returns (new_settings, changed). Replaces any existing
unverified-tag-ledger entry of this hook type with fragment's, appends if none
existed yet."""
settings = json.loads(json.dumps(settings)) # deep copy, no external dep
entry_list = settings.setdefault("hooks", {}).setdefault(hook_type, [])
new_entries = fragment["hooks"][hook_type]

before = json.dumps(entry_list, sort_keys=True)
entry_list[:] = [e for e in entry_list if not _is_ours(e, marker)] + new_entries
changed = json.dumps(entry_list, sort_keys=True) != before
return settings, changed


def main():
settings = {}
if os.path.exists(SETTINGS_PATH):
with open(SETTINGS_PATH) as f:
settings = json.load(f)

any_changed = False
for hook_type, marker, fragment_path in HOOK_SPECS:
with open(fragment_path) as f:
fragment = json.load(f)
settings, changed = merge_hook(settings, hook_type, marker, fragment)
if changed:
any_changed = True
print(f"link claude {hook_type} hook merged into settings.json")
else:
print(f"ok claude {hook_type} hook already up to date")

if not any_changed:
return

os.makedirs(os.path.dirname(SETTINGS_PATH), exist_ok=True)
with open(SETTINGS_PATH, "w") as f:
json.dump(settings, f, indent=2)
f.write("\n")
print(" (restart Claude Code to pick up the change)")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ fi
# either file. See each script's docstring for exactly what it does.
echo "--- claude Stop + UserPromptSubmit hooks (\$HOME/.claude/settings.json) ---"
python3 "$REPO_DIR/engine/hooks/diu-stop/install_claude_hook.py"
python3 "$REPO_DIR/engine/hooks/unverified-tag-ledger/install_claude_hook.py"
python3 "$REPO_DIR/engine/hooks/bug-complaint-leak/install_claude_hook.py"
python3 "$REPO_DIR/engine/hooks/reflect-on-thrash/install_claude_hook.py"
python3 "$REPO_DIR/engine/hooks/scope-lock/install_claude_hook.py"
Expand Down
Loading