← back to Ticket Guard
install.sh: gated, idempotent, self-verifying settings.json wiring (TK-11621)
ffe9706bbf303939f9e6b7e4e5ead37ab99a5521 · 2026-09-13 16:29:38 -0700 · Steve
Refuses to wire if the negative test fails. Backs up settings.json. Rehearsed on a
copy: additive only (UserPromptSubmit 5->6), idempotent, other hook events untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkVx1x2ppxm6tdEXngBj2P
Files touched
Diff
commit ffe9706bbf303939f9e6b7e4e5ead37ab99a5521
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Sep 13 16:29:38 2026 -0700
install.sh: gated, idempotent, self-verifying settings.json wiring (TK-11621)
Refuses to wire if the negative test fails. Backs up settings.json. Rehearsed on a
copy: additive only (UserPromptSubmit 5->6), idempotent, other hook events untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkVx1x2ppxm6tdEXngBj2P
---
install.sh | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 70 insertions(+)
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..df31b1d
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+# install.sh — wire ticket-guard.sh into settings.json as a UserPromptSubmit hook.
+# GATED: edits ~/.claude/settings.json, which the auto-mode classifier blocks the
+# agent from touching. Steve runs this himself. Idempotent + self-verifying.
+#
+# install: bash ~/Projects/ticket-guard/install.sh
+# uninstall: bash ~/Projects/ticket-guard/install.sh --uninstall
+set -euo pipefail
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SETTINGS="$HOME/.claude/settings.json"
+HOOKDIR="$HOME/.claude/hooks"
+CMD='$HOME/.claude/hooks/ticket-guard.sh'
+MODE="${1:-install}"
+
+[ -f "$SETTINGS" ] || { echo "FATAL: $SETTINGS not found"; exit 1; }
+
+# 1. Prove the guard works BEFORE wiring it. A hook that cannot go red is not worth
+# installing, and a broken hook runs on every prompt in ~77 sessions.
+if [ "$MODE" != "--uninstall" ]; then
+ echo "== negative test =="
+ bash "$REPO/test-ticket-guard.sh" || { echo "FATAL: negative test failed — NOT wiring"; exit 1; }
+fi
+
+# 2. Back up settings.json (the undo).
+BAK="$SETTINGS.bak-ticketguard-$(date +%Y%m%d-%H%M%S)"
+cp "$SETTINGS" "$BAK"; echo "== backup: $BAK"
+
+# 3. Install / remove the hook script itself.
+if [ "$MODE" = "--uninstall" ]; then
+ rm -f "$HOOKDIR/ticket-guard.sh"; echo "== removed $HOOKDIR/ticket-guard.sh"
+else
+ mkdir -p "$HOOKDIR"; cp "$REPO/ticket-guard.sh" "$HOOKDIR/ticket-guard.sh"
+ chmod +x "$HOOKDIR/ticket-guard.sh"; echo "== installed $HOOKDIR/ticket-guard.sh"
+fi
+
+# 4. Edit settings.json (idempotent; validated before it replaces the live file).
+SETTINGS="$SETTINGS" CMD="$CMD" MODE="$MODE" python3 - <<'PY'
+import json, os, sys
+p, cmd, mode = os.environ["SETTINGS"], os.environ["CMD"], os.environ["MODE"]
+d = json.load(open(p))
+hooks = d.setdefault("hooks", {})
+ups = hooks.setdefault("UserPromptSubmit", [])
+def has(entry):
+ return any(h.get("command") == cmd for h in entry.get("hooks", []))
+present = any(has(e) for e in ups)
+if mode == "--uninstall":
+ if not present:
+ print("== settings.json: hook not present, nothing to remove"); sys.exit(0)
+ hooks["UserPromptSubmit"] = [e for e in ups if not has(e)]
+ action = "removed from"
+else:
+ if present:
+ print("== settings.json: hook ALREADY wired (idempotent no-op)"); sys.exit(0)
+ ups.append({"hooks": [{"type": "command", "command": cmd}]})
+ action = "added to"
+out = json.dumps(d, indent=2)
+json.loads(out) # validate before writing
+open(p, "w").write(out + "\n")
+print(f"== settings.json: ticket-guard {action} UserPromptSubmit")
+PY
+
+# 5. Verify the live file is still valid JSON and report the wired state.
+python3 -c "
+import json;d=json.load(open('$SETTINGS'))
+ups=d.get('hooks',{}).get('UserPromptSubmit',[])
+n=sum(1 for e in ups for h in e.get('hooks',[]) if 'ticket-guard' in h.get('command',''))
+print(f'== VERIFY: settings.json parses OK; ticket-guard entries = {n}')
+"
+echo "== done. New sessions pick this up immediately; already-running sessions need /clear or a restart."
+echo "== undo: bash $REPO/install.sh --uninstall (or: cp $BAK $SETTINGS)"
← 153f410 ticket-guard: UserPromptSubmit hook that catches ticketless
·
back to Ticket Guard
·
harden ticket-guard after second-model review: pid+start ide 1d2b313 →