diff --git a/.github/workflows/sync-hook-events.yml b/.github/workflows/sync-hook-events.yml new file mode 100644 index 00000000..bd2802fe --- /dev/null +++ b/.github/workflows/sync-hook-events.yml @@ -0,0 +1,69 @@ +name: Sync Hook Event Types + +on: + schedule: + - cron: '7 8 * * *' # daily at 08:07 UTC + workflow_dispatch: + +jobs: + sync-hooks: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Install Claude Code CLI + run: npm install -g @anthropic-ai/claude-code + + # Claude only gets its own API credentials — no GITHUB_TOKEN + - name: Analyze hook coverage and edit files + env: + ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} + ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} + FAILPROOFAI_TELEMETRY_DISABLED: "1" + CLAUDE_SKIP_SETUP_MODAL: "true" + run: | + claude \ + --model claude-sonnet-4-6 \ + --allowedTools "Read,Edit,Glob,Grep,WebFetch" \ + --dangerously-skip-permissions \ + -p "$(cat scripts/sync-hook-events-prompt.md)" + + # PR creation is handled here — GITHUB_TOKEN never touches Claude + - name: Create PR if changes detected + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RESULT=$(cat .sync-hook-events-output.json 2>/dev/null || echo '{"changed":false}') + CHANGED=$(echo "$RESULT" | jq -r '.changed') + + if [ "$CHANGED" != "true" ]; then + echo "Hook coverage is up to date. No PR needed." + exit 0 + fi + + # Check for an existing open sync PR to avoid duplicates + EXISTING=$(gh pr list --base main --search "[auto] sync hook event types" --state open --json number --jq length) + if [ "$EXISTING" -gt 0 ]; then + echo "Sync PR already open. Skipping." + exit 0 + fi + + BRANCH="auto/sync-hook-events-$(date +%Y%m%d-%H%M)" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add src/hooks/types.ts __tests__/hooks/manager.test.ts + git commit -m "feat: sync hook event types with Claude Code docs" + git push origin "$BRANCH" + + PR_TITLE=$(echo "$RESULT" | jq -r '.prTitle') + PR_BODY=$(echo "$RESULT" | jq -r '.prBody') + gh pr create \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --base main \ + --head "$BRANCH" diff --git a/.gitignore b/.gitignore index 42455312..c3df3a33 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ next-env.d.ts # custom hooks loader temp files *.__failproofai_tmp__.* +# sync-hook-events workflow output (ephemeral, generated in CI) +.sync-hook-events-output.json + # package manager lockfiles (bun.lock is tracked; bun.lockb is binary) package-lock.json diff --git a/__tests__/hooks/manager.test.ts b/__tests__/hooks/manager.test.ts index 24684361..9994241c 100644 --- a/__tests__/hooks/manager.test.ts +++ b/__tests__/hooks/manager.test.ts @@ -57,7 +57,7 @@ describe("hooks/manager", () => { }); describe("installHooks", () => { - it("installs hooks for all 17 event types into empty settings", async () => { + it("installs hooks for all 26 event types into empty settings", async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFileSync).mockReturnValue("{}"); @@ -69,7 +69,7 @@ describe("hooks/manager", () => { expect(path).toBe(USER_SETTINGS_PATH); const written = JSON.parse(content as string); - expect(Object.keys(written.hooks)).toHaveLength(17); + expect(Object.keys(written.hooks)).toHaveLength(26); for (const [eventType, matchers] of Object.entries(written.hooks)) { expect(matchers).toHaveLength(1); @@ -217,7 +217,7 @@ describe("hooks/manager", () => { expect(writeFileSync).toHaveBeenCalledOnce(); const [, content] = vi.mocked(writeFileSync).mock.calls[0]; const written = JSON.parse(content as string); - expect(Object.keys(written.hooks)).toHaveLength(17); + expect(Object.keys(written.hooks)).toHaveLength(26); }); it("uses 'where' on Windows and handles multi-line output", async () => { diff --git a/package.json b/package.json index 8e08969f..d059847d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "failproofai", - "version": "0.0.1-beta.11", + "version": "0.0.1-beta.12", "description": "Open-source hooks, policies, and project visualization for Claude Code & Agents SDK", "bin": { "failproofai": "./bin/failproofai.mjs" diff --git a/scripts/sync-hook-events-prompt.md b/scripts/sync-hook-events-prompt.md new file mode 100644 index 00000000..04bdc0ea --- /dev/null +++ b/scripts/sync-hook-events-prompt.md @@ -0,0 +1,60 @@ +You are an automated agent running in GitHub Actions to keep failproofai's hook +event types in sync with the official Claude Code documentation. + +## Your task + +1. Fetch the Claude Code hooks reference pages using WebFetch: + - https://code.claude.com/docs/en/hooks (full reference — has the complete event table) + - https://code.claude.com/docs/en/hooks-guide (guide — also has a summary event table) + Extract the complete list of all hook event type names (e.g. SessionStart, + PreToolUse, PostToolUse, etc.) from the event lifecycle/trigger table. + Use both pages; union the results if they differ. Prefer the reference page. + +2. Read `src/hooks/types.ts` and extract the current `HOOK_EVENT_TYPES` array + (the TypeScript `as const` array of strings). + +3. Diff the two lists: + - **added**: event types in the docs but NOT in our array + - **removed**: event types in our array but NOT in the docs + +4. If there are NO differences: + Write the following JSON to `.sync-hook-events-output.json` in the repo root: + ```json + { "changed": false } + ``` + Then stop. + +5. If there are differences: + + a. Update `HOOK_EVENT_TYPES` in `src/hooks/types.ts`: + - Add new event types (append after the last existing entry, before `] as const`) + - Remove stale event types if any + + b. Update `__tests__/hooks/manager.test.ts` — find the hardcoded event-type + counts and update them to the new total: + - The test description string matching `all N event types` + - The `toHaveLength(N)` assertion(s) that check `Object.keys(written.hooks)` + Search by the current count number to locate them. + + c. Write the following JSON to `.sync-hook-events-output.json` in the repo root: + ```json + { + "changed": true, + "added": ["EventA", "EventB"], + "removed": ["EventC"], + "prTitle": "[auto] sync hook event types with Claude Code docs", + "prBody": "..." + } + ``` + The `prBody` must be a Markdown string containing: + - List of **added** event types (or "none") + - List of **removed** event types (or "none") + - Source URLs used + - A note: "CI must pass and this PR must be reviewed before merging." + +## Constraints + +- **Only edit `src/hooks/types.ts`, `__tests__/hooks/manager.test.ts`, and + `.sync-hook-events-output.json`**. No other files. +- Do NOT run any shell commands (no git, no gh, no bun). +- Do NOT modify `policy-evaluator.ts`, `manager.ts`, or any other source file. diff --git a/src/hooks/types.ts b/src/hooks/types.ts index 2c8e3351..9adbe409 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -10,19 +10,28 @@ export const HOOK_EVENT_TYPES = [ "SessionEnd", "UserPromptSubmit", "PreToolUse", + "PermissionRequest", + "PermissionDenied", "PostToolUse", "PostToolUseFailure", - "PermissionRequest", "Notification", "SubagentStart", "SubagentStop", + "TaskCreated", + "TaskCompleted", "Stop", + "StopFailure", "TeammateIdle", - "TaskCompleted", + "InstructionsLoaded", "ConfigChange", + "CwdChanged", + "FileChanged", "WorktreeCreate", "WorktreeRemove", "PreCompact", + "PostCompact", + "Elicitation", + "ElicitationResult", ] as const; export type HookEventType = (typeof HOOK_EVENT_TYPES)[number];