Files
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

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_NAME plus the full tool input JSON on stdin (and a 16 KB-truncated copy in JCODE_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 call jcode CLI commands without re-triggering hooks in that nested process.