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
148 lines
5.1 KiB
Markdown
148 lines
5.1 KiB
Markdown
# 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](SPAWN_HOOK.md) (which controls *where headed
|
|
sessions appear*); lifecycle hooks tell you *what is happening inside them*.
|
|
|
|
## Configuration
|
|
|
|
```toml
|
|
# ~/.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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
#!/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`:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# ~/bin/jcode-event-log
|
|
echo "$JCODE_HOOK_PAYLOAD" >> ~/.local/state/jcode-events.jsonl
|
|
```
|
|
|
|
```toml
|
|
[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.
|