← 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)"