5.1 KiB
Lifecycle Hooks
jcode can run external commands at well-defined lifecycle points so other programs can observe or gate agent behavior without forking jcode. Hooks complement the spawn hook (which controls where headed sessions appear); lifecycle hooks tell you what is happening inside them.
Configuration
# ~/.jcode/config.toml
[hooks]
turn_end = "~/bin/jcode-turn-notify" # observer
session_start = "" # observer
session_end = "" # observer
pre_tool = "~/bin/jcode-tool-policy" # gate
post_tool = "" # observer
pre_tool_timeout_ms = 5000
Env overrides (always win; empty value disables a config hook):
JCODE_HOOK_TURN_END, JCODE_HOOK_SESSION_START, JCODE_HOOK_SESSION_END,
JCODE_HOOK_PRE_TOOL, JCODE_HOOK_POST_TOOL, JCODE_HOOK_PRE_TOOL_TIMEOUT_MS.
Common contract
- The hook command line is parsed shell-style (quotes and backslash escapes
work) but executed directly, not through a shell. A leading
~/in the program path is expanded. - The hook runs in the session working directory when known.
- Every hook receives:
| Variable | Meaning |
|---|---|
JCODE_HOOK_EVENT |
turn_end, session_start, session_end, pre_tool, post_tool |
JCODE_HOOK_SESSION_ID |
Session the event belongs to |
JCODE_HOOK_CWD |
Session working directory |
JCODE_HOOK_PAYLOAD |
JSON object mirroring all fields (capped at 16 KB) |
JCODE_HOOKS_DISABLED |
Always 1; suppresses hooks in nested jcode calls (recursion guard) |
Observer hooks
turn_end, session_start, session_end, and post_tool are
observers: spawned detached, fire-and-forget. They can never block or slow
the agent; failures are only logged.
turn_end
Fires when an agent turn completes (streaming turn path, which covers TUI, desktop, swarm workers, and headless sessions).
Extra fields: JCODE_HOOK_STATUS (ok/error), JCODE_HOOK_DURATION_MS,
JCODE_HOOK_MODEL, JCODE_HOOK_LAST_ASSISTANT_TEXT (first 4000 chars),
JCODE_HOOK_ERROR (on failure).
session_start / session_end
session_start fires when an agent session becomes active, with
JCODE_HOOK_SOURCE = create (brand new), attach (existing session object
attached), or resume (restored by id). session_end fires on normal close
(JCODE_HOOK_SOURCE=close).
post_tool
Fires after every tool call. Extra fields: JCODE_HOOK_TOOL_NAME,
JCODE_HOOK_STATUS, JCODE_HOOK_DURATION_MS, JCODE_HOOK_OUTPUT_BYTES (on
success), JCODE_HOOK_ERROR (on failure).
Gate hook: pre_tool
pre_tool runs synchronously before every tool call and can block it:
- The hook receives
JCODE_HOOK_TOOL_NAMEplus the full tool input JSON on stdin (and a 16 KB-truncated copy inJCODE_HOOK_TOOL_INPUT). - Exit 0: allow the call.
- Exit 2: block the call. The hook's stderr (trimmed, capped at 2000 chars) is returned to the model as the tool error, so the model can adapt.
- Anything else fails open with a logged warning: other exit codes,
timeout (
pre_tool_timeout_ms, default 5s), missing binary, spawn errors.
Fail-open is deliberate: a broken policy script should degrade to "no policy" rather than brick every session. If you need fail-closed semantics, make the hook itself robust (it is your trust boundary, not jcode).
Example policy script
#!/usr/bin/env bash
# ~/bin/jcode-tool-policy
# stdin: tool input JSON. Env: JCODE_HOOK_TOOL_NAME, JCODE_HOOK_SESSION_ID...
input=$(cat)
case "$JCODE_HOOK_TOOL_NAME" in
bash)
if grep -qE 'rm -rf /([^a-zA-Z]|$)|mkfs|dd if=' <<<"$input"; then
echo "blocked: destructive shell command" >&2
exit 2
fi
;;
write|edit)
if grep -q '"file_path":"/etc/' <<<"$input"; then
echo "blocked: writes to /etc are not allowed" >&2
exit 2
fi
;;
esac
exit 0
Example: tmux status + desktop notification on turn end
#!/usr/bin/env bash
# ~/bin/jcode-turn-notify
if [ "$JCODE_HOOK_STATUS" = ok ]; then icon=✅; else icon=❌; fi
tmux display-message "jcode $icon ${JCODE_HOOK_SESSION_ID:0:12}" 2>/dev/null
notify-send "jcode turn $JCODE_HOOK_STATUS" \
"${JCODE_HOOK_LAST_ASSISTANT_TEXT:0:120}" 2>/dev/null
exit 0
Example: JSON event log of all hook activity
Point several hooks at one script and fan out on JCODE_HOOK_EVENT:
#!/usr/bin/env bash
# ~/bin/jcode-event-log
echo "$JCODE_HOOK_PAYLOAD" >> ~/.local/state/jcode-events.jsonl
[hooks]
turn_end = "~/bin/jcode-event-log"
session_start = "~/bin/jcode-event-log"
session_end = "~/bin/jcode-event-log"
post_tool = "~/bin/jcode-event-log"
Design notes
- Hook lookups are config-driven and re-read on config reload; you can add or change hooks without restarting jcode.
- Hot paths (
pre_tool/post_tool) check whether a hook is configured before building any payload, so unconfigured hooks cost ~nothing. - The recursion guard (
JCODE_HOOKS_DISABLED=1) means a hook may safely calljcodeCLI commands without re-triggering hooks in that nested process.