-
Notifications
You must be signed in to change notification settings - Fork 0
fix: resolve 7 v1.4 bugs + remove orphaned segments #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,63 @@ | ||
| # Philosophy | ||
|
|
||
| hookwise was born from watching Claude Code do amazing things -- and occasionally terrifying things. | ||
| hookwise is a platform for guarding, coaching, and observing Claude Code sessions. | ||
|
|
||
| ## Why hookwise Exists | ||
|
|
||
| I built hookwise because I kept waking up to disasters. | ||
| Claude Code is extraordinary. It can refactor an entire module, set up infrastructure, write tests -- all while you are reading the previous diff. That power is exactly why it needs guard rails, observability, and coaching. | ||
|
|
||
| Not "my server is down" disasters -- "my AI rewrote the build system while I was making coffee" disasters. Claude Code is extraordinary. It can refactor an entire module, set up infrastructure, write tests -- all while you are reading the previous diff. That power is exactly why it needs guard rails. | ||
| The existing hooks system is powerful but raw. You write bash scripts, scatter them across your config, and hope they cover the right events. There is no testing, no sharing, no analytics, and no way to know if your guards actually work until something slips through. | ||
|
|
||
| The existing hooks system is powerful but raw. You write bash scripts, scatter them across your config, and hope they cover the right events. There is no testing, no sharing, and no way to know if your guards actually work until something slips through. | ||
| hookwise replaces all of that with a platform: declarative guards, analytics that tell you where your time actually goes, coaching that nudges you when you have been in tooling mode for 90 minutes, feeds that surface context in your status line, recipes that let you share configurations, and a TUI that ties it all together. | ||
|
|
||
| **hookwise is one YAML file that replaces all of that.** Declarative guards that read like firewall rules. Analytics that tell you where your time actually goes. Coaching that nudges you when you have been in "tooling mode" for 90 minutes and forgot your original goal. | ||
| ## Core Principles | ||
|
|
||
| ## Design Principle | ||
| ### 1. Platform, Not Just Config | ||
|
|
||
| The design principle is simple: **your AI should never be the reason you cannot sleep.** Everything in hookwise serves that principle -- guards protect, context enriches, side effects observe. If any part of hookwise itself errors, it fails open. hookwise must never be the thing that breaks your flow. | ||
| hookwise started as a YAML config layer for guard rails. It is now a platform with a SQLite analytics engine, a cache bus, coaching state, a feed daemon, a TUI, and a recipe system. YAML remains the right format for declaring guards, coaching rules, and feed settings -- the things that should be human-readable, git-tracked, and declarative. But YAML is not the whole platform. | ||
|
|
||
| > *"Guard rails should be boring. The exciting part is what you build when you are not worried about what your AI is doing."* | ||
| The platform needs a data layer, not just a config layer. Recipes need a standard way to persist structured data. Analytics needs a durable store. Coaching needs memory across sessions. Without a standard data layer, every recipe author invents their own storage, every new feature adds another ad hoc file, and the platform fragments. | ||
|
|
||
| ## Fail-Open | ||
| hookwise should provide a shared data layer that any subsystem or recipe can use. The right tool for that layer -- SQLite, a managed database, purpose-built files -- is an engineering question, not a philosophical one. | ||
|
|
||
| If any part of hookwise itself errors, it fails open. hookwise must never accidentally block a tool call due to internal errors. Any unhandled exception anywhere in the dispatch pipeline results in `exit 0`. | ||
| ### 2. Degrade Gracefully, Always Warn | ||
|
|
||
| If any part of hookwise errors, the session continues. hookwise must never accidentally block a tool call due to internal errors. Any unhandled exception in the dispatch pipeline results in `exit 0`. | ||
|
|
||
| But graceful degradation must not mean silent degradation. The v1.3 retro showed that "fail-open" was interpreted as "fail-silent" -- 5 of 7 launch bugs shipped unnoticed because errors were swallowed without any signal to the user. Every degradation must produce a visible diagnostic: a status line warning, a cache bus key, a log entry. The user should always know when something is running in a reduced state. | ||
|
|
||
| **Degrade gracefully AND always warn the user.** | ||
|
|
||
| Graceful degradation is a design practice, not a philosophical argument against infrastructure. A database server that crashes should not take down your Claude Code session -- but the possibility of a crash is not a reason to avoid running a database server. The question is whether the infrastructure solves a real problem and whether hookwise handles its failure modes correctly. | ||
|
|
||
| ### 3. Infrastructure Is Normal | ||
|
|
||
| hookwise is a developer tool platform. Developer tool platforms have databases, background daemons, and managed processes. This is normal and expected. | ||
|
|
||
| hookwise already runs a background daemon for feeds, manages PID files, bundles better-sqlite3 as a native addon, and ships a Python TUI package. The bar for adding infrastructure should be: "Does this solve a real problem for users?" Not: "Is this zero-config?" and not: "Could this possibly go down?" | ||
|
|
||
| What matters is that infrastructure is invisible to the user when it works, and diagnostic when it does not. | ||
|
|
||
| ### 4. Guards Should Be Boring; the Platform Should Not | ||
|
|
||
| Guard rails should be declarative, predictable, and unexciting. You write YAML rules that read like firewall policies. First block wins. No magic. The exciting part is what you build when you are not worried about what your AI is doing. | ||
|
|
||
| This principle applies specifically to the guard system. Other parts of hookwise -- analytics dashboards, coaching nudges, feed-backed status lines, the TUI, recipe ecosystems -- can and should be rich, interactive, and engaging. The platform grows; guards stay boring. | ||
|
|
||
| ### 5. Recipes Need a Data Contract | ||
|
|
||
| A recipe is a shareable hookwise configuration: guards, coaching rules, feed producers, status line segments. Today, a recipe is a directory with `hooks.yaml`. That is enough for declarative configuration but not enough for recipes that need to persist data. | ||
|
|
||
| A recipe that tracks code review patterns needs a table. A recipe that scores commit quality needs a rolling window of scores. A recipe that detects dependency drift needs a snapshot of the last lockfile. Without a standard data layer, each recipe author builds their own persistence -- different formats, different locations, different cleanup strategies. | ||
|
|
||
| hookwise should define a data contract for recipes: a standard way to create tables, read/write rows, and query data. The underlying engine is an implementation detail. The contract is what makes recipes portable and composable. | ||
|
|
||
| ### 6. Composition Over Extension | ||
|
|
||
| New capabilities should be composable from existing subsystems. A new feed producer is a function. A new guard is a YAML rule. A new recipe is a directory with `hooks.yaml` and optionally a data schema. A new status line segment is a pure function of cache and config. | ||
|
|
||
| When composition is not enough and a new architectural layer is needed, that is a signal to design it carefully -- not a signal to avoid it. | ||
|
|
||
| --- | ||
|
|
||
| ← [Back to Home](/) | ||
| <- [Back to Home](/) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| #!/usr/bin/env bash | ||
| # hooks/agent-tracker.sh | ||
| # | ||
| # Claude Code hook for SubagentStart / SubagentStop events. | ||
| # Reads JSON from stdin, maintains state at ~/.hookwise/cache/active-agents.json. | ||
| # Always exits 0 (fail-open). | ||
| set -uo pipefail | ||
|
|
||
| STATE_DIR="${HOOKWISE_STATE_DIR:-$HOME/.hookwise}" | ||
| CACHE_DIR="$STATE_DIR/cache" | ||
| STATE_FILE="$CACHE_DIR/active-agents.json" | ||
| STALE_SECONDS=600 # 10 minutes | ||
|
|
||
| mkdir -p "$CACHE_DIR" | ||
|
|
||
| # Read stdin JSON | ||
| INPUT=$(cat) | ||
|
|
||
| # Extract event type from CLAUDE_CODE_HOOK_EVENT_NAME env var | ||
| EVENT="${CLAUDE_CODE_HOOK_EVENT_NAME:-}" | ||
|
|
||
| # Parse fields from stdin JSON | ||
| agent_id=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('agent_id',''))" 2>/dev/null || echo "") | ||
| session_id=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('session_id',''))" 2>/dev/null || echo "") | ||
| worktree=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || echo "") | ||
| team_name=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('team_name',''))" 2>/dev/null || echo "") | ||
| strategy=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('strategy',''))" 2>/dev/null || echo "") | ||
|
|
||
| if [ -z "$agent_id" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Derive a short name from worktree basename or truncated agent_id | ||
| if [ -n "$worktree" ]; then | ||
| name=$(basename "$worktree") | ||
| else | ||
| name="${agent_id:0:12}" | ||
| fi | ||
|
|
||
| NOW=$(date +%s) | ||
|
|
||
| # Read existing state (or start fresh) | ||
| if [ -f "$STATE_FILE" ]; then | ||
| CURRENT=$(cat "$STATE_FILE" 2>/dev/null || echo '{"agents":[],"team_name":"","strategy":""}') | ||
| else | ||
| CURRENT='{"agents":[],"team_name":"","strategy":""}' | ||
| fi | ||
|
|
||
| # Use python3 for JSON manipulation (available on macOS) | ||
| UPDATED=$(python3 -c " | ||
| import json, sys | ||
|
|
||
| current = json.loads('''$CURRENT''') | ||
| agents = current.get('agents', []) | ||
| event = '$EVENT' | ||
| agent_id = '$agent_id' | ||
| name = '$name' | ||
| now = $NOW | ||
| stale = $STALE_SECONDS | ||
| team_name = '$team_name' or current.get('team_name', '') | ||
| strategy = '$strategy' or current.get('strategy', '') | ||
|
|
||
|
Comment on lines
+50
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline Python interpolation is injection-prone and brittle. Unescaped values ( 🔒 Safer pattern (pass data as argv/stdin, not source interpolation)-UPDATED=$(python3 -c "
-import json, sys
-current = json.loads('''$CURRENT''')
-event = '$EVENT'
-agent_id = '$agent_id'
-name = '$name'
-now = $NOW
-stale = $STALE_SECONDS
-team_name = '$team_name' or current.get('team_name', '')
-strategy = '$strategy' or current.get('strategy', '')
-...
-print(json.dumps(result, indent=2))
-" 2>/dev/null) || exit 0
+UPDATED=$(
+python3 - "$CURRENT" "$EVENT" "$agent_id" "$name" "$NOW" "$STALE_SECONDS" "$team_name" "$strategy" <<'PY'
+import json, sys
+current_json, event, agent_id, name, now_s, stale_s, team_name, strategy = sys.argv[1:]
+current = json.loads(current_json) if current_json else {"agents": [], "team_name": "", "strategy": ""}
+now = int(now_s)
+stale = int(stale_s)
+agents = current.get("agents", [])
+team_name = team_name or current.get("team_name", "")
+strategy = strategy or current.get("strategy", "")
+# ...existing mutation logic...
+print(json.dumps({"agents": agents, "team_name": team_name, "strategy": strategy, "updated_at": now}, indent=2))
+PY
+) 2>/dev/null || exit 0Also applies to: 66-89 🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 6cf685d. Changed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainLet me check the actual commit to verify what was changed. 🏁 Script executed: Length of output: 431 🏁 Script executed: Length of output: 2594
The # Still in the current file (lines ~50+):
UPDATED=$(python3 -c "
current = json.loads('''$CURRENT''') # $CURRENT is a JSON blob — breakable with ''' in any value
event = '$EVENT'
agent_id = '$agent_id' # single-quote in agent_id breaks Python syntax
name = '$name'
team_name = '$team_name' or ...
strategy = '$strategy' or ...
")Why this is still risky: The safer fix from my original suggestion — passing values via UPDATED=$(
python3 - "$CURRENT" "$EVENT" "$agent_id" "$name" "$NOW" "$STALE_SECONDS" "$team_name" "$strategy" <<'PY'
import json, sys
current_json, event, agent_id, name, now_s, stale_s, team_name, strategy = sys.argv[1:]
current = json.loads(current_json) if current_json else {"agents": [], "team_name": "", "strategy": ""}
now = int(now_s)
stale = int(stale_s)
agents = current.get("agents", [])
team_name = team_name or current.get("team_name", "")
strategy = strategy or current.get("strategy", "")
# ... rest of mutation logic unchanged ...
PY
) 2>/dev/null || trueThis pattern is immune to quote injection because values never touch Python source — they're passed as process arguments and read via |
||
| # Clean stale entries (older than 10 minutes) | ||
| agents = [a for a in agents if (now - a.get('started_at', now)) < stale] | ||
|
|
||
| if event == 'SubagentStart': | ||
| # Remove existing entry with same agent_id (if re-started) | ||
| agents = [a for a in agents if a.get('agent_id') != agent_id] | ||
| agents.append({ | ||
| 'agent_id': agent_id, | ||
| 'name': name, | ||
| 'status': 'working', | ||
| 'started_at': now | ||
| }) | ||
| elif event == 'SubagentStop': | ||
| for a in agents: | ||
| if a.get('agent_id') == agent_id: | ||
| a['status'] = 'done' | ||
| a['stopped_at'] = now | ||
| break | ||
|
|
||
| result = { | ||
| 'agents': agents, | ||
| 'team_name': team_name, | ||
| 'strategy': strategy, | ||
| 'updated_at': now | ||
| } | ||
| print(json.dumps(result, indent=2)) | ||
| " 2>/dev/null) || exit 0 | ||
|
|
||
| # Atomic write via temp file + rename | ||
| TMPFILE="$CACHE_DIR/.active-agents.tmp.$$" | ||
| echo "$UPDATED" > "$TMPFILE" | ||
| mv "$TMPFILE" "$STATE_FILE" | ||
|
|
||
| exit 0 | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a lock around read-modify-write to prevent lost updates.
Concurrent SubagentStart/SubagentStop hooks can race: both read old state, then last writer wins and drops the other update.
Also applies to: 91-95
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferring. The values interpolated into the Python block come from Claude Code's hook event JSON payload — a trusted internal source, not user input. The
agent_id,name, etc. are system-generated identifiers. Agreed that stdin-based passing would be more robust, but the injection risk here is theoretical given the trusted source.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vishnujayvel, just a quick clarification — my original concern was specifically about concurrent execution race conditions (two Claude Code hook processes running simultaneously, both reading the oldSTATE_FILEbefore either has written back, so one update silently overwrites the other), rather than injection risk from the payload values.Your point about the trusted source is valid for the injection angle, but the race condition is independent of data trust — it's a filesystem concurrency issue. That said, since you're deferring, understood! If you open a follow-up issue later, the fix would be something like:
No action needed from your side right now.