chore: import upstream snapshot with attribution
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
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
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user