#!/usr/bin/env node /** * scripts/setup-hooks.mjs — install/remove web-terminal's Claude Code hooks (H2). * * Adds `command` hooks to ~/.claude/settings.json that POST each hook event to * the web-terminal server so it can show live Claude status per tab. The command * is an inline curl that uses two env vars the server injects into each spawned * shell: $WEBTERM_HOOK_URL (the server's /hook URL, with the right port) and * $WEBTERM_SESSION (which tab). Outside web-terminal those vars are unset, so * curl no-ops (|| true) — the hooks are harmless in normal terminals. * * Usage: * node scripts/setup-hooks.mjs # install (idempotent) * node scripts/setup-hooks.mjs --remove # uninstall * * Backs up settings.json to settings.json.webterm-bak before writing. */ import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs' import { homedir } from 'node:os' import path from 'node:path' const SETTINGS = path.join(homedir(), '.claude', 'settings.json') const MARKER = 'WEBTERM_HOOK_URL' // identifies our hook entries const COMMAND = 'curl -s -X POST "$WEBTERM_HOOK_URL" -H "X-Webterm-Session: $WEBTERM_SESSION"' + ' -H "Content-Type: application/json" --data-binary @- >/dev/null 2>&1 || true' // Events we care about (status derives from the event/notification_type server-side). const EVENTS = ['UserPromptSubmit', 'PreToolUse', 'Notification', 'PermissionRequest', 'Stop', 'SessionEnd'] const remove = process.argv.includes('--remove') let settings = {} if (existsSync(SETTINGS)) { try { settings = JSON.parse(readFileSync(SETTINGS, 'utf8')) } catch { console.error(`${SETTINGS} is not valid JSON — aborting (fix it first).`) process.exit(1) } copyFileSync(SETTINGS, `${SETTINGS}.webterm-bak`) } if (typeof settings.hooks !== 'object' || settings.hooks === null) settings.hooks = {} // Strip any existing web-terminal hook groups (makes install idempotent + powers --remove). for (const ev of Object.keys(settings.hooks)) { const groups = settings.hooks[ev] if (!Array.isArray(groups)) continue settings.hooks[ev] = groups.filter( (g) => !( g && Array.isArray(g.hooks) && g.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(MARKER)) ), ) if (settings.hooks[ev].length === 0) delete settings.hooks[ev] } if (!remove) { for (const ev of EVENTS) { if (!Array.isArray(settings.hooks[ev])) settings.hooks[ev] = [] settings.hooks[ev].push({ hooks: [{ type: 'command', command: COMMAND }] }) } } mkdirSync(path.dirname(SETTINGS), { recursive: true }) writeFileSync(SETTINGS, `${JSON.stringify(settings, null, 2)}\n`) console.log( remove ? `Removed web-terminal hooks from ${SETTINGS}` : `Installed web-terminal hooks into ${SETTINGS} (events: ${EVENTS.join(', ')})`, ) if (existsSync(`${SETTINGS}.webterm-bak`)) console.log(`Backup: ${SETTINGS}.webterm-bak`) console.log('Restart any running `claude` sessions for the hooks to take effect.')