Extend LoopCode
Hooks
Run your own commands at fixed points in LoopCode's agent loop — format after edits, block dangerous commands, filter test output, add context, or keep the agent working. Compatible with Claude Code hooks.
Why hooks
A prompt asks the model to do something; a hook makes it happen. Use hooks for anything that must not be skipped: formatting after every edit, refusing a dangerous command, filtering a huge test log down to the failures before it reaches the context window, or writing an audit record.
Hooks are ordinary programs — written in whatever your team already uses — configured in settings files. The events, payloads and decisions have the same names Claude Code uses, so hooks written for it mostly work here unchanged.
Events
| Event | When | Can |
|---|---|---|
PreToolUse | Before a tool call runs | Allow, deny or ask; rewrite the tool's input |
PostToolUse | After a tool call | Add context for the model |
UserPromptSubmit | When you send a message | Rewrite the prompt; add context; block it |
SessionStart | When a session starts | Add context |
Stop | When the agent is about to finish its turn | Keep it working, with an instruction |
SubagentStop | When a subagent finishes | Keep it working |
PreCompact | Before the conversation is compacted | Add instructions for the summary |
Configure
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|MultiEdit|Write", "hooks": [{ "type": "command", "command": "npx prettier --write \"$(jq -r .tool_input.path)\"" }] }
],
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "./scripts/hooks/no-prod-db.sh", "timeout": 10 }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "./scripts/hooks/require-changelog.sh" }] }
]
}
}| Field | Meaning |
|---|---|
matcher | For tool events: a tool name, a | list (Edit|Write), or a regular expression. Omit it, or use *, for every call |
hooks[].command | The command to run, through your shell, in the project folder |
hooks[].timeout | Seconds before it is stopped (default 60, at most 600) |
hooks[].async | true runs it without waiting; its result is ignored. For notifications and logging |
Hooks from all settings files accumulate. /hooks lists the hooks in force and which files they came from.
What a hook receives
The event as JSON on standard input:
{
"hook_event_name": "PreToolUse",
"session_id": "…",
"cwd": "/Users/you/code/shop",
"permission_mode": "auto",
"tool_name": "Bash",
"tool_input": { "command": "psql $PROD_DATABASE_URL -c 'drop table orders'" }
}| Field | Present for |
|---|---|
tool_name, tool_input | Tool events |
tool_result (text, ok) | PostToolUse |
prompt | UserPromptSubmit |
session_start_source | SessionStart |
last_assistant_message, stop_hook_active | Stop and SubagentStop. Check stop_hook_active to avoid sending the agent back in a loop |
agent_type | SubagentStop |
trigger (auto or manual), custom_instructions | PreCompact |
The environment also has LOOPCODE_PROJECT_DIR (the project folder) and LOOPCODE_HOOK_EVENT.
What a hook can decide
Exit codes. 0 is success. 2 blocks the action, with whatever the hook wrote to standard error as the reason, which the agent sees. Any other exit code is treated as a broken hook — reported, but not a veto.
JSON on standard output, at the top level or under hookSpecificOutput:
| Field | Effect |
|---|---|
permissionDecision | PreToolUse: allow, deny or ask, overriding the permission check for this call |
permissionDecisionReason | Why — shown to the agent |
updatedInput | PreToolUse: replace fields of the tool's input before it runs |
updatedPrompt | UserPromptSubmit: replace the prompt before the model sees it |
additionalContext | Any event: text handed to the model as extra context |
systemMessage | A message for you, shown in the transcript |
decision: "block" with reason | Stop: keep the agent working, with reason as its instruction. PreToolUse: deny |
#!/bin/sh
command=$(jq -r '.tool_input.command // ""')
case "$command" in
*PROD_DATABASE_URL*) echo "Never touch the production database from an agent." >&2; exit 2 ;;
esac
exit 0
Safety
Hooks run with your permissions, without asking. A repository's committed .loopcode/settings.json can define hooks, so read the settings of a repository you do not trust before working in it. "disableAllHooks": true turns every hook off — put it in that project's .loopcode/settings.local.json, which is read last.