Bundled Claude Code hooks

Hooks are shell commands that run at specific points in the agent's lifecycle. Ideafy ships two hooks that talk to the Ideafy local server:

Hook eventWhen it firesWhat the server does
UserPromptSubmitEvery time you submit a messageInjects context-aware policy (phase-aware reminders, creation offers, offline notices)
PreToolUse (matcher: Edit|Write|NotebookEdit|MultiEdit)Just before the agent runs an edit-family toolDenies the tool call if you're bound to an in-progress card but on the wrong branch; allows otherwise

Both hooks fail open — if the Ideafy server is unreachable, neither one can wedge your editor. You keep working; Ideafy just stops tracking.

Platform support

Hooks are a Claude-Code-only feature. Codex and Gemini's CLIs don't expose an equivalent lifecycle hook yet, so the hook endpoints are no-ops for those platforms — POST /api/projects/[id]/hook returns {installed: true, applicable: false} and writes nothing to disk. This keeps the Ideafy MCP & Skills toggle's state check consistent across all three platforms while avoiding stray .claude/settings.json files in non-Claude projects.

On Claude Code, hooks reach your project through two channels:

  • Plugin delivery — the Claude Code plugin bundles both hooks inside its hooks/hooks.json. When the plugin is enabled (global or project scope), Claude Code reads the hooks directly from the plugin cache; nothing is written to <project>/.claude/settings.json.
  • Classic delivery — the per-project Ideafy MCP & Skills toggle writes the hook entries directly into <project>/.claude/settings.json. The behaviour of the hooks is identical; only the install location differs.

Prefer the plugin for Claude Code. Use classic delivery if you're on Gemini / Codex (where only the MCP + skills parts of that toggle actually apply) or if you explicitly want hooks written to the project file.

How the UserPromptSubmit hook works

On every prompt submit, the hook sends a POST request to http://localhost:<port>/api/hook-context with Claude Code's session JSON on stdin. The request also forwards the IDEAFY_CARD_ID environment variable (if set) as a card_hint query parameter.

The server inspects the session and responds with one of three behaviours:

Session stateServer response
New session, no card hintReturns a creation offer policy — a one-time prompt that asks the agent to propose creating a card when the user's request looks like trackable work
Session bound to a cardReturns a phase-aware policy — a reminder tailored to the card's current column that tells the agent which save tool to propose when work is done
Offered (already showed the creation prompt), terminal column, or unrecognised project204 No Content — the hook is silent

If the Ideafy server is unreachable (not running), the hook emits a short offline reminder asking the agent to offer launching Ideafy once, then stay silent for the rest of the session.

Phase-aware policy

When a session is bound to a card, the hook returns a <system-reminder> block with instructions specific to the card's current kanban column:

ColumnExpected action
IdeationPropose save_opinion. Positive verdict moves to Backlog; negative verdict also calls move_card to Withdrawn
BacklogPropose save_plan. Moves to In Progress
BugsPropose save_plan. Moves to In Progress
In ProgressPropose save_tests. Moves to Human Test
Test / Completed / WithdrawnNo policy — the hook returns 204 (terminal columns)

The policy also enforces a confirmation protocol: the agent must stop and ask the user for permission before calling any save_* tool. On "yes" it calls immediately; on "no" it continues working and re-asks only after meaningful progress.

For cards in In Progress specifically, the policy appends an extra clause telling the agent which git branch the card must be implemented on, and to call ensure_branch before the first edit if the working tree is on the wrong branch. This runs in tandem with the PreToolUse hook described below — the policy gives Claude advance notice; the hook is the hard backstop.

Creation offer policy

When you open Claude Code in a registered project folder without a card hint, the hook shows a one-time creation offer on the first turn. The agent proposes creating a card in the appropriate column based on your request:

  • A new idea that needs evaluation -> Ideation
  • A known task ready to plan -> Backlog
  • A bug report or broken behaviour -> Bugs

If you say "yes", the agent calls create_card followed by bind_session_to_card, and the phase-aware policy kicks in from the next turn. If you say "no" or your request is a quick lookup, the offer doesn't repeat.

Session binding

The hook tracks sessions via the ideafy_sessions table. A session can be bound to a card in three ways:

  1. Card hint (legacy) — when Ideafy spawns a terminal on a specific card, it sets IDEAFY_CARD_ID. The hook auto-binds the session on the first turn.
  2. Creation offer — the agent creates a card and calls bind_session_to_card with the new card ID.
  3. Explicit binding — the user names an existing card (e.g. "this is for IDE-125") and the agent calls bind_session_to_card directly.

Once bound, every subsequent turn receives the phase-aware policy for that card's current column.

The PreToolUse hook: branch enforcement

Binding a session to a card promises the card will be implemented on its own branch, but the UserPromptSubmit hook alone cannot enforce that — it can only ask Claude politely. The PreToolUse hook closes the gap. It runs immediately before any Edit, Write, NotebookEdit, or MultiEdit tool call and blocks edits that would land on the wrong branch.

What it posts

The hook sends Claude Code's PreToolUse payload to http://localhost:<port>/api/pre-edit-check on stdin. The endpoint walks a short decision tree:

ConditionDecision
Tool name is not Edit / Write / NotebookEdit / MultiEditAllow (204 No Content)
Session is not bound to a cardAllow
Card is not in progressAllow — enforcement only applies during implementation
Effective worktree policy is false (card override, or project default off)Allow — user opted into main-branch work
Target branch can't be resolved (no gitBranchName, no taskNumber+project)Allow
Working tree is already on the target branchAllow
Any unexpected errorAllow (204) — fail-open, editor never wedged
OtherwiseDeny with a remediation message

The effective policy follows a strict hierarchy: card-level useWorktree wins, then project-level useWorktrees, default true.

What Claude sees on deny

The deny response includes a human-readable reason that names the target branch, the current branch, and the exact MCP tool to call to fix it:

Ideafy: this card must be implemented on branch "kanban/KAN-14-add-google-oauth",
but the current working tree (/path/to/project) is on "main".
Call mcp__ideafy__ensure_branch with cardId "<uuid>" to fix this, then retry the edit.

Claude receives this as a permission decision from its hook runner, aborts the edit, calls ensure_branch, and retries — all within the same agent turn. From your perspective the edit "pauses, switches branches, and completes" automatically. You only notice when something is genuinely misconfigured (e.g. the card's project folder isn't a git repo).

Why this matters

Two loopholes existed before this hook:

  1. Manual bind_session_to_card from an existing shell — you open Claude Code in the project root, ask it to work on IDE-42, the agent binds the session, and then edits main because no worktree was provisioned by a /start or /open-terminal route.
  2. Sessions that survive column moves — a card was in Backlog when the session started, got moved to In Progress later, and Claude kept editing wherever it was before.

Both cases now deny the first wrong-branch edit and redirect through ensure_branch.

When it's silent

In all the conditions that return "Allow" above. In particular, terminal columns don't enforce (Test / Completed / Withdrawn / Ideation). Once the card leaves In Progress, you're free to edit wherever; the tests phase is for exercising what's already built, not for writing new features.

When is IDEAFY_CARD_ID set?

Ideafy sets the env var only when it spawns a terminal or background process on a specific card — autonomous, quick-fix, evaluate, interactive terminal. Outside those contexts (you running Claude Code directly from your shell, not from Ideafy), the variable is unset. The hook still fires (it talks to the server using the session ID and working directory), but relies on folder-based project resolution and session binding rather than the env var.

Installing and removing

POST /api/projects/[id]/hook installs both hooks into the project's .claude/settings.json in a single merge. DELETE removes both. The project settings modal exposes this as part of the combined Ideafy MCP & Skills toggle, which installs hook + MCP + skills together — see MCP & Skills installation. The install is idempotent — the installer strips any previous Ideafy entries (identified by the # ideafy-hook or # ideafy-pre-edit-check markers, plus legacy IDEAFY_CARD_ID / KANBAN_CARD_ID substrings) before writing the current canonical pair.

There is also a bulk endpoint: POST /api/projects/reinstall-hooks reinstalls across all registered projects at once — useful after an Ideafy update that ships a new hook body. The Install Hooks button in the main Settings modal fires this. Existing projects pick up the newer hook entries this way without opening each project's settings. Note that this button only touches hooks; MCP servers and skills are not reinstalled by it — use the per-project toggle for those.

If you want to verify, open .claude/settings.json in the project root and look for:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "# ideafy-hook\ncurl -sf -X POST ... \"http://localhost:${IDEAFY_PORT:-3030}/api/hook-context?card_hint=${IDEAFY_CARD_ID:-}\" || printf '...offline reminder...'"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Edit|Write|NotebookEdit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "# ideafy-pre-edit-check\ncurl -sf -X POST ... \"http://localhost:${IDEAFY_PORT:-3030}/api/pre-edit-check\" 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

The IDEAFY_PORT environment variable defaults to 3030 but can be overridden if you run Ideafy on a different port.

Writing your own hooks

Ideafy doesn't get in the way of you adding more hooks. The hook array is part of your project's config, not Ideafy's — you can append additional hooks to UserPromptSubmit, PreToolUse, PostToolUse, Stop, or any other event. Ideafy manages only its own two canonical entries (identified by the # ideafy-hook and # ideafy-pre-edit-check markers). Your additions are preserved across install/uninstall, including sibling entries under the same events — the installer filters by marker, not by array position.

See the Claude Code documentation for the full list of hook events and their capabilities.

When to turn the hooks off

You almost never need to. But if you want to:

  • Click Uninstall Ideafy Hooks in the project settings modal (removes both), or
  • Edit .claude/settings.json by hand and remove the entries that start with # ideafy-hook and # ideafy-pre-edit-check

Card terminal sessions still work without the hook — you just lose the automatic context injection, so the agent won't know which card you're working on or which column it's in. Use the other two workflow anchors instead (mention the card ID directly, or stay in the card modal chat panel which already has the card context baked in).


Prev: Bundled Claude Code skills Next: MCP tools reference Up: User guide index

Last updated: 2026-04-23