[object Object]

← 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 →