Skills are opt-in (I have to remember to invoke them). Hooks are
ambient — they fire on every matching event automatically. This PR
moves the careful-mode and learnings discipline from "doc I should
read" to "harness-enforced behavior I cannot bypass".
## 6 new hooks (.claude/hooks/)
- pre-bash-careful — REFUSES git push --force to main, rm -rf at root,
DROP TABLE against prod schema. WARNs on force-with-lease, gh pr/
issue close. Tested: blocks the destructive case, allows safe ones.
- pre-edit-freeze — implements /freeze. When .claude/freeze contains
a path glob, edits outside it are denied. Tested: edits to PLAN.md
blocked when scope locked to platform/internal/handlers/.
- session-start-context — auto-loads last 20 cron-learnings, freeze
status, open-PR/issue counts as additionalContext at session start.
Tested: emits valid SessionStart JSON.
- post-edit-audit — appends every Edit/Write to .claude/audit.jsonl
(gitignored). One-line records {ts, tool, file, ok}. Tested writes.
- user-prompt-tag — injects context warnings when prompt mentions
force-push, drop-table, "delete all", "push to main", etc. Tested:
emits warning for "force push the fix to main".
- subagent-stop-judge — off by default; touch .claude/judge-subagents
to enable. When on, prompts orchestrator to verify subagent's last
message addresses the original task. Cost-free MVP (no LLM call yet).
All hooks are Python (jq isn't on the hook PATH on macOS — Python is).
Shared helpers in _lib.py: read_input, deny_pretooluse, add_context,
warn_to_stderr.
## settings.json — wires all 6 hooks
Adds SessionStart, UserPromptSubmit, SubagentStop event handlers.
Existing PreToolUse:Bash + PostToolUse:Edit chains gain the new hooks
alongside the existing ones (check-inbox.sh, echo reminder).
Adds @modelcontextprotocol/server-sequential-thinking MCP server for
structured chain-of-thought scratchpad — useful when triaging multiple
PRs in parallel without losing context.
## .claude/commands/triage.md — slash command shortcut
Manual /triage runs the same flow as the c5074cd5 hourly cron, on
demand. Saves ~4KB of prompt every invocation by pulling the cron
prompt out of working memory.
## CLAUDE.md additions
New "Agent operating rules (auto-loaded — read first)" section right
after Ecosystem Context. Documents:
- Cron / triage discipline (read learnings, treat docs PRs touching
CLAUDE.md/PLAN.md as noteworthy, write per-tick reflections)
- Table of all 6 hooks active in this repo
- List of skills and how to invoke them
- Standing rules (inviolable) consolidated for the agent
This block auto-loads into every conversation context — free behavior
change without me remembering to opt in.
## .gitignore
audit.jsonl, freeze, judge-subagents, per-tick-reflections.md are all
local operational state, never committed.
## Verification
- echo '{"tool_input":{"command":"git push --force origin main"}}' |
bash pre-bash-careful.sh → emits deny JSON ✓
- Same for git status (safe command) → empty output, exit 0 ✓
- pre-edit-freeze with .claude/freeze=platform/handlers/ blocks
edits to PLAN.md, allows edits inside the locked path ✓
- post-edit-audit appends valid JSONL ✓
- session-start-context emits additionalContext with PR/issue counts ✓
- user-prompt-tag emits warning for "force push to main" prompt ✓
- python3 -c "json.load(open('.claude/settings.json'))" → valid ✓
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
63 lines
2.9 KiB
Python
Executable File
63 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""PreToolUse:Bash — enforce careful-mode patterns on shell commands."""
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from _lib import read_input, deny_pretooluse, warn_to_stderr # noqa
|
|
|
|
|
|
def main() -> None:
|
|
data = read_input()
|
|
cmd = data.get("tool_input", {}).get("command", "")
|
|
if not cmd:
|
|
return
|
|
|
|
# REFUSE list — hard stops
|
|
refuse_patterns = [
|
|
("git push --force", "main", "git push --force to main is REFUSED. Use --force-with-lease on a feature branch only."),
|
|
("git push -f", "main", "git push -f to main is REFUSED."),
|
|
("git push --force", "master", "git push --force to master is REFUSED."),
|
|
("git push -f", "master", "git push -f to master is REFUSED."),
|
|
]
|
|
for needle1, needle2, msg in refuse_patterns:
|
|
if needle1 in cmd and needle2 in cmd:
|
|
deny_pretooluse(f"careful-mode: {msg}")
|
|
|
|
if "git reset --hard" in cmd and ("origin/main" in cmd or " main" in cmd or "/main" in cmd):
|
|
deny_pretooluse("careful-mode: git reset --hard against main is REFUSED. Stash, branch, then reset.")
|
|
|
|
# SQL DDL/DML against prod-like names
|
|
sql_destructive = ["DROP TABLE", "DROP DATABASE", "TRUNCATE TABLE"]
|
|
for tok in sql_destructive:
|
|
if tok in cmd:
|
|
# Allow against test/sandbox patterns
|
|
allow_substrings = ["_test", "sandbox", "/tmp/", "_dev", "test_"]
|
|
if not any(a in cmd for a in allow_substrings):
|
|
deny_pretooluse(f"careful-mode: '{tok}' against production-like schema is REFUSED. Use a migration with explicit review.")
|
|
|
|
# rm -rf at scary paths
|
|
if "rm -rf" in cmd:
|
|
scary = [" /", " ~", " $HOME", "/.git ", "/.git/"]
|
|
scratch_ok = ["/tmp/", "node_modules", "dist", ".next", "__pycache__", ".pytest_cache", "coverage"]
|
|
if any(s in cmd for s in scary) and not any(s in cmd for s in scratch_ok):
|
|
# Check for migrations dir specifically
|
|
if "migrations" in cmd:
|
|
deny_pretooluse("careful-mode: rm -rf inside a migrations dir is REFUSED.")
|
|
deny_pretooluse(f"careful-mode: rm -rf at filesystem root, HOME, or .git is REFUSED. Command: {cmd[:200]}")
|
|
if "/.git" in cmd:
|
|
deny_pretooluse("careful-mode: rm -rf .git is REFUSED. Re-clone if you need a fresh repo.")
|
|
|
|
# WARN list — log but allow
|
|
if "git push --force-with-lease" in cmd:
|
|
warn_to_stderr("[careful-mode WARN] force-with-lease: safer than --force but still rewrites remote history.")
|
|
if "gh pr close" in cmd or "gh issue close" in cmd:
|
|
warn_to_stderr("[careful-mode WARN] closing a PR/issue is irreversible from this bot's standpoint. Confirm intent.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as e: # never break tool execution due to hook bug
|
|
warn_to_stderr(f"[careful-mode hook error] {e}")
|
|
sys.exit(0)
|