Session Hooks
A hook is a point where PeckBoard pauses what it is doing and hands the moment to an installed plugin — to observe it, veto it, rewrite it, or own it outright. This page maps every hook onto the life of a session, drawn as a tree: the trunk is the session moving from idle through a turn and back, the shoots are hooks, and several shoots loop back into the trunk rather than ending.
The Turn Tree
Everything starts and ends at an idle session. Sending a message grows the tree downward — through message hooks, into the agent’s turn, out to tool calls — and every completed turn is a shoot that bends all the way back to the root.
graph TD
IDLE([Session idle]):::root
MB{{session.message.before}}:::hook
PH[Pre-hatch research session]
PHA{{session.prehatch.answer}}:::hook
PHC{{session.prehatch.cancel}}:::hook
SRR{{session.reference.resolve}}:::hook
TURN[Agent turn]
PS{{provider.send}}:::hook
TCB{{mcp.tool.call.before}}:::hook
TI{{mcp.tool.invoke}}:::hook
CORE[Core tool handler]
TCA{{mcp.tool.call.after}}:::hook
TCF{{mcp.tool.call.failed}}:::hook
QP{{question.pending}}:::hook
UA{{session.user.answer}}:::hook
TDO{{todo}}:::hook
AE{{session.agent.ended}}:::hook
IDLE -->|you send a message| MB
MB -->|allow, text may be rewritten| SRR
MB -->|cancel: a plugin takes the turn| PH
PH -->|you answer its question card| PHA
PHA --> SRR
PH -.->|you cancel the pre-hatch| PHC
PHC -.->|original message delivered| SRR
SRR --> TURN
TURN -->|model comes from a plugin| PS
PS --> TURN
TURN -->|tool call| TCB
TCB -->|plugin-owned tool| TI
TCB -->|core tool| CORE
TI --> TCA
CORE -->|success| TCA
CORE -->|error| TCF
TCA -->|result returned| TURN
TCF -->|error returned| TURN
TURN -->|agent asks you something| QP
QP -->|you answer| UA
UA --> TURN
TURN -->|raw output batch| TDO
TDO --> TURN
TURN -->|turn ends or crashes| AE
AE -->|back to the root| IDLE
classDef hook fill:#fdece7,stroke:#b93000
classDef root fill:#fff3cf,stroke:#c98d00
Reading down the trunk: session.message.before fires before a chat message reaches the agent — a plugin may rewrite the text or take the whole turn, which is how pre-hatcher works; its question cards come back through session.prehatch.answer and session.prehatch.cancel, both of which bend back to the trunk by delivering a message. session.reference.resolve lets a plugin expand a session: reference in the text into something richer. During the turn, every tool call passes mcp.tool.call.before, is answered either by the plugin that owns the tool (mcp.tool.invoke) or by core, and reports back through mcp.tool.call.after or mcp.tool.call.failed — then the shoot rejoins the turn. Questions to you fan out through question.pending and return as session.user.answer. When the turn finishes, session.agent.ended fires and the tree folds back to the root, idle again.
Observing hooks versus owning hooks
Most hooks observe an operation core is performing: the plugin can allow it, cancel it, or modify its payload, and core carries on. Four hooks instead hand the plugin the whole operation: mcp.tool.invoke (the plugin’s own tool call — core only routes and scopes it), provider.send (one full agent turn on a plugin-registered model), and the two HTTP hooks described below. Notification hooks — question.pending, session.agent.ended, todo, and the board notifications — ignore the verdict entirely; by the time they fire, the thing has happened.
The todo hook is the odd shoot: for providers that are themselves plugins, each batch of raw agent output is offered to a todo-hook plugin, which can distill it into the session’s current work-item list — the same list the Claude provider extracts natively.
Setup and Teardown
The turn tree only exists because the session was wired up first. When a session that talks to the MCP server is created, its access token and config file pass through hooks on the way in — and on the way out when the session is deleted.
graph TD
BORN[Session created] --> TIB{{mcp.token.issue.before}}:::hook
TIB --> TIA{{mcp.token.issue.after}}:::hook
TIA --> CWB{{mcp.config.write.before}}:::hook
CWB --> CWA{{mcp.config.write.after}}:::hook
CWA --> IDLE([Session idle]):::root
IDLE --> GONE[Session deleted]
GONE --> CDA{{mcp.config.delete.after}}:::hook
CDA --> TRA{{mcp.token.revoke.after}}:::hook
classDef hook fill:#fdece7,stroke:#b93000
classDef root fill:#fff3cf,stroke:#c98d00
mcp.token.issue.before can veto issuing the session’s bearer token, and mcp.config.write.before can modify the MCP config the agent will launch with; their .after counterparts, and the delete/revoke pair, are notifications.
Around the Board
A worker session grows the same turn tree, but its shoots also reach the board — and the board loops back to the root harder than anything else: every completed step returns control to the project scheduler, which grows a fresh tree for the next step or card.
graph TD
SCHED([Project scheduler]):::root
W[Worker session — its own turn tree]
CCB{{card.create.before}}:::hook
CUB{{card.update.before}}:::hook
CSA{{card.step.after}}:::hook
WB{{worker.blocked}}:::hook
PP{{project.paused}}:::hook
SCHED -->|picks the top ready card| W
W -->|create_card tool| CCB
W -->|update_card tool| CUB
CCB -.-> W
CUB -.-> W
W -->|step completed or card moved| CSA
CSA -->|next step, next worker| SCHED
W -->|repeated crashes| WB
WB --> PP
PP -->|project paused, tree stops growing| STOP([Paused]):::root
classDef hook fill:#fdece7,stroke:#b93000
classDef root fill:#fff3cf,stroke:#c98d00
card.create.before and card.update.before fire whether the change comes from a worker’s tool call or from you in the UI, and can veto or adjust the card. card.step.after is the loop-back shoot: each step advance returns to the scheduler. The two right-hand shoots are the emergency exit — repeated crashes fire worker.blocked together with project.paused, and that branch does not return.
Outside the Tree
Five hooks are not part of any session’s tree. http.request.before and http.request.authed serve plugin HTTP routes — public /plugin-api/* and signed-in /api/plugin-ui/* respectively — where the plugin owns the request end to end; this is how plugin pages like the diff viewer exist. provider.register fires at startup and whenever the plugin set changes, asking each provider plugin to register its models, and the optional provider.models fires on every model-catalog read, letting that plugin refresh the list without being re-registered. card.priorities.list fires when the priority options are listed, letting a plugin add to them.
Every hook at a glance
| Hook | Fires | Plugin can |
|---|---|---|
session.message.before |
Before a chat message reaches the agent | Rewrite the text, or take the turn |
session.prehatch.answer |
You answer a pre-hatch question card | Own the outcome |
session.prehatch.cancel |
You cancel an in-flight pre-hatch | Own the fallback delivery |
session.reference.resolve |
A session: reference appears in a message |
Replace it with custom text |
provider.send |
A turn runs on a plugin-registered model | Own the entire turn |
provider.interrupt |
You interrupt a turn on a plugin model | Release what the turn owns upstream |
mcp.tool.call.before |
Before any MCP tool call | Cancel it or modify the arguments |
mcp.tool.invoke |
A plugin-owned tool is called | Own the call and return its result |
mcp.tool.call.after |
A tool call succeeded | Observe |
mcp.tool.call.failed |
A tool call failed | Observe |
question.pending |
A session surfaces a question for you | Observe |
session.user.answer |
You answer an ask_user question |
Observe |
todo |
A batch of raw plugin-provider output arrives | Report the session’s work items |
session.agent.ended |
An agent run completes or crashes | Observe |
mcp.token.issue.before |
Before a session’s MCP token is issued | Cancel |
mcp.token.issue.after |
After the token is issued | Observe |
mcp.token.revoke.after |
After the token is revoked | Observe |
mcp.config.write.before |
Before the session’s MCP config is written | Cancel or modify the config |
mcp.config.write.after |
After the config is written | Observe |
mcp.config.delete.after |
After the config is cleaned up | Observe |
card.create.before |
Before a card is created | Cancel or adjust the fields |
card.update.before |
Before a card is updated | Cancel or adjust the fields |
card.step.after |
After a card changes step | Observe |
card.priorities.list |
When priority options are listed | Add or modify options |
worker.blocked |
A card’s crashes pause its project | Observe |
project.paused |
A project pauses (manual, crash, or budget) | Observe |
provider.register |
Startup, or the plugin set changes | Register a provider and models |
provider.models |
The model catalog is read | Refresh its model list |
http.request.before |
A public /plugin-api/* request arrives |
Own the request and response |
http.request.authed |
A signed-in /api/plugin-ui/* request arrives |
Own the request and response |