← back to Ticket Guard
install.sh
71 lines
#!/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)"