Bundled Claude Code hooks

Hooks are shell commands that run at specific points in the agent's lifecycle. Ideafy ships three 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
PostToolUse (matcher: ExitPlanMode)Right after you approve a plan in plan modeNudges the agent to write the approved plan onto the card, but only when the card's Solution tab is genuinely empty

All three fail open — if the Ideafy server is unreachable, none of them 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 the same 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. The tool does not move the card — after the opinion is saved the agent asks separately whether to move it (Backlog on a positive verdict, Withdrawn on a negative one) and calls move_card only on a clear yes
BacklogPropose save_plan. Moves to In Progress
BugsPropose save_plan. Moves to In Progress
In ProgressPropose save_tests. Moves to Human Test
Human TestIts own policy — record verified scenarios with save_tests as you go, then ask once about moving to Completed (see below)
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.

Why Ideation doesn't move the card. save_opinion writes the Opinion tab and nothing else — its SQL only ever touched ai_opinion and ai_verdict. The policy used to claim the tool moved a positive verdict to Backlog, so the agent reported a move that never happened and you found the card still sitting in Ideation. The sentence was removed rather than the behaviour added: writing an opinion and deciding to commit to the work are two decisions, and only the second one deserves a column change. A test now checks, for every column in the policy, that a move the text promises is a move the handler actually performs.

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.

The commit-reference clauses

Whenever the bound card has a resolvable display ID, the policy appends two more clauses about commit messages:

  1. When a commit advances the work this card describes, reference the card with a trailer — Card: IDE-283 on its own line as the last line of the commit body. The subject is written exactly as it otherwise would be: no prefix, no change in style.
  2. Do not tag a commit with this card when the work is something the card doesn't cover. Untagged commits are legitimate and common. Leave those untagged, or ask in one sentence whether to open a card for the work and use that card's ref instead.

The second clause is the point of the pair. A wrong ref is worse than none — it makes the board claim something that is not true. See How a commit says which card it came from.

The display ID is validated by shape (PREFIX-123) rather than escaped before it reaches the reminder; anything else is dropped outright. Clause numbering is counted rather than hardcoded, because the phase blocks are different lengths — seven clauses for the standard columns, four for testing.

The Human Test policy

Human Test is the one column that doesn't fit the shape above, so it gets its own policy block instead of the generic "do the work, then ask before the one save_* call that ends the phase".

The reason is that in every other column there is exactly one save at the end. In testing, recording a result is the work — it happens many times in a session — and making each one wait for a confirmation round trip would be pure noise. So the policy splits the two:

ActionNeeds your permission?
Ticking a scenario the agent just verified and calling save_testsNo — it does it as it goes
Moving the card to CompletedYes — asked once, in a single short sentence

The four rules the agent receives:

  1. Whenever it verifies a scenario itself, mark it [x] and call save_tests with the full checklist — every existing item with its current state, changing only the boxes it verified.
  2. Never check a scenario it didn't actually observe. It stays [ ] with a reason — needs a person, needs access the agent doesn't have, or it failed. A failing scenario stays unchecked and gets reported, never quietly skipped.
  3. When every scenario is checked, stop and ask whether to move the card to Completed. On a clear yes, call move_card with status completed in the same turn.
  4. On "no", keep working — "no" means "not yet". Don't re-ask on turns where nothing new was verified.

Before this policy existed, /api/hook-context answered 204 the moment a card landed in Human Test. A session testing a card was given no policy at all, so nothing told it to record what it had just verified — scenarios stayed unchecked until you asked by hand.

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.

When you already asked for a card

The policy opens with an exception clause, settled before anything else: if your message already asks for a card — "Ideafy'da bir kart aç", "create a card for this", "bunu backlog'a ekle" — you have already given permission. The agent doesn't ask. It picks the column from your wording, calls create_card and bind_session_to_card in that same turn, and carries on with the work you actually asked for.

Nothing was broken before this clause existed. The policy said to ask, and said that asking ends the turn, and both were followed — the permission you'd already given simply had nowhere to land. So it has to be read before the asking clauses, not after them.

The clause is careful about its own edges. A message that merely mentions cards is not a request for one: a question about how card creation behaves, or a complaint that the agent asked when it shouldn't have, wants an answer — not a card. When the agent is unsure, it falls through to the ordinary "ask once" clause.

The keyword signal

The hook also runs a small keyword scan over the turn's prompt (Turkish and English: kart aç, kartı oluştur, create a card, card for this, ideafy'a ekle, …). A match appends one extra line to the policy saying the wording matched — and explicitly calling it a hint, not a command.

It is deliberately never the decision. "kart aç dememe rağmen hook yine soruyor" is a complaint about card creation, not a request for one — and it contains every keyword a real request contains. No keyword list can tell those apart. Getting it wrong in the other direction is the expensive case: a false positive that read as a command would open a card on the exact turn where you were asking why cards keep getting opened.

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.

The PostToolUse hook: plan sync

Plan mode writes the approved plan to a file under ~/.claude/plans, and nothing ever told the card about it. A session bound to a card could finish planning, the plan would live in a markdown file nobody opens again, and the card's Solution tab would stay empty — with no signal anywhere that the two had come apart. It surfaced later as "why isn't the plan on the card", by which point the work had moved on.

The phase policy can't cover this. It fires on user prompts and knows only which column the card is in; it never learns that a plan was just produced. So this hook fires exactly once, at approval.

It posts Claude Code's PostToolUse payload to /api/plan-sync-check, and the endpoint stays silent unless every one of these holds:

ConditionOtherwise
The tool was ExitPlanMode204
The session is bound to a card204
The card still exists204
The card's Solution tab is empty204

That last check is the important one. A card that already has a plan gets silence, because a second nudge per plan is worse than none. "Empty" is measured after stripping HTML — Solution is stored as Tiptap HTML, so an untouched field can be a lone empty paragraph rather than "".

When all four hold, Claude receives a note saying the plan currently exists only in the plan file, asking it to check with you before writing it to the card with save_plan — and reminding it to mention that save_plan also moves the card to In Progress if it isn't there yet.

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 all three hooks into the project's .claude/settings.json in a single merge. DELETE removes all three. The project settings modal exposes this as part of the combined Ideafy MCP & Skills toggle, which installs hooks + MCP + skills together — see MCP & Skills installation. The install is idempotent — the installer strips any previous Ideafy entries (identified by the # ideafy-hook, # ideafy-pre-edit-check or # ideafy-plan-sync-check markers, plus legacy IDEAFY_CARD_ID / KANBAN_CARD_ID substrings) before writing the current canonical set.

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"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "ExitPlanMode",
        "hooks": [
          {
            "type": "command",
            "command": "# ideafy-plan-sync-check\ncurl -sf -X POST ... \"http://localhost:${IDEAFY_PORT:-3030}/api/plan-sync-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 three canonical entries (identified by the # ideafy-hook, # ideafy-pre-edit-check and # ideafy-plan-sync-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. Notably, your own PostToolUse hooks survive Ideafy's install even though Ideafy now writes to that event too.

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 all three), or
  • Edit .claude/settings.json by hand and remove the entries that start with # ideafy-hook, # ideafy-pre-edit-check and # ideafy-plan-sync-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-08-20