Hooks
Hooks let you observe, control, and extend the agent loop. CFactory supports two complementary surfaces that share one lifecycle:
- Declarative hooks — Cursor-compatible
hooks.json+ command/prompt scripts (stdin/stdout JSON) - TypeScript plugins — the existing
@cfactory/pluginHooksAPI
Both see the same agent events in the same order.
What you can do
- Run formatters after edits
- Gate risky shell or MCP calls (
allow/deny/ask) - Block sensitive file reads
- Validate prompts before submit
- Inject session context or env at session start
- Follow up when the agent stops (
stop/subagentStop)
Configuration
| Source | Path | Script cwd |
|---|---|---|
| Managed (MDM) | /Library/Application Support/cfactory/hooks.json (or platform equivalent) | managed config dir |
| Organization | ~/.cache/cfactory/org/<orgId>/hooks/hooks.json (pulled via org sync) | org hooks cache |
| Project | .cfactory/hooks.json | project root |
| Global | ~/.config/cfactory/hooks.json | ~/.config/cfactory/ |
| Cursor compat | .cursor/hooks.json | project root |
Precedence (highest wins on updated_input / messages): managed > org > project > cursor > global. permission: deny always wins. When cfactory.org.hooks.enforce (or hooks.enforce from /api/config) is true, an org/managed deny cannot be relaxed by lower-priority sources.
Org hooks are never copied into the git worktree — only into the org cache, same as skills/commands.
Example
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": ".cfactory/hooks/block-network.sh",
"matcher": "curl|wget|nc ",
"failClosed": true
}
],
"afterFileEdit": [
{
"command": ".cfactory/hooks/format.sh"
}
]
}
}
Command hooks receive JSON on stdin and may print JSON on stdout.
- Exit
0— use JSON output - Exit
2— deny (same aspermission: "deny") - Other non-zero — fail-open unless
failClosed: true
Environment variables: CFACTORY_PROJECT_DIR, CFACTORY_VERSION, and CLAUDE_PROJECT_DIR (alias).
Agent events
| Event | Control |
|---|---|
sessionStart / sessionEnd | Inject env / additional_context; audit end |
preToolUse / postToolUse / postToolUseFailure | Deny/rewrite input; inject additional_context |
subagentStart / subagentStop | Allow/deny Task; followup_message |
beforeShellExecution / afterShellExecution | Allow/deny/ask shell; audit output |
beforeMCPExecution / afterMCPExecution | Allow/deny/ask MCP |
beforeReadFile / afterFileEdit | Deny reads; post-process edits |
beforeSubmitPrompt | Block submit (continue: false) |
preCompact | Observe compaction |
stop | followup_message with loop_limit (default 5) |
afterAgentResponse / afterAgentThought | Observe assistant text / reasoning |
Deferred (not in v1)
- Tab hooks (
beforeTabFileRead,afterTabFileEdit) workspaceOpen- Enterprise MDM / cloud team sync
Plugin mapping
| hooks.json / Cursor | Plugin hook |
|---|---|
preToolUse | preToolUse (+ legacy tool.execute.before) |
postToolUse | postToolUse (+ legacy tool.execute.after) |
beforeSubmitPrompt | beforeSubmitPrompt (+ chat.message) |
preCompact | preCompact (+ experimental.session.compacting for mutation) |
afterAgentResponse | afterAgentResponse (+ experimental.text.complete) |
| — | shell.env, chat.params, auth, provider, config, event (plugin-only) |
See Plugins for the TypeScript API.
Debugging
Hook runs are logged by the CLI and published on the event bus as hooks.run (start / success / error / denied / skipped). In the IDE, open Settings → Agent Behaviour → Hooks.