Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9c142b8
chore: ignore .omo directory
quangdang46 May 26, 2026
863dcd9
feat(hooks): implement Command hook execution via tokio::process
quangdang46 May 26, 2026
7ec4646
feat(hooks): implement HookRegistry with event filtering
quangdang46 May 26, 2026
119e589
feat(hooks): implement Command hook execution via tokio::process
quangdang46 May 26, 2026
51da951
feat(hooks): add multi-layer hooks config loading
quangdang46 May 26, 2026
6dde6bc
feat(hooks): implement CLI hooks command handlers (list, add, remove)
quangdang46 May 26, 2026
d8815b0
feat(hooks): integrate PreToolUse/PostToolUse hooks in tool execution
quangdang46 May 27, 2026
3cf691d
fix(hooks): resolve compilation errors - tracing dep, lifetime fixes,…
quangdang46 May 27, 2026
8de8b6f
feat(hooks): integrate SessionStart/End and Permission hooks in Registry
quangdang46 May 27, 2026
a0b1fcf
docs: add hooks section to CONFIG_REFERENCE
quangdang46 May 27, 2026
373f310
feat(hooks): implement HTTP hook handler
quangdang46 May 27, 2026
42dfcd9
feat(hooks): add ToolError hook and new HookEvent variants
quangdang46 May 27, 2026
f1fd686
feat(hooks): integrate PermissionRequest/PermissionDenied hooks
quangdang46 May 27, 2026
b308b34
fix(cli): implement enable/disable hooks commands
quangdang46 May 27, 2026
951e159
fix(hooks): derive Deserialize/Serialize for HookMatcher
quangdang46 May 27, 2026
b518be2
feat(hooks): integrate SessionStart/SessionEnd hooks
quangdang46 May 27, 2026
0e6d62e
fix(hooks): wire matcher and condition accessors properly
quangdang46 May 27, 2026
ee6ed26
Add JCODE_HOOKS_CONFIG env var override layer to load_hooks_config
quangdang46 May 27, 2026
f48c161
fix(hooks): add lifetime annotations to get_handler_matcher/get_handl…
quangdang46 May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ ios_simulator_screenshot.png
/.wrangler/
/tmp/
/.jcode/generated-images/
/.omo/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ similar = "2" # diffing for edits
# Utilities
dirs = "5" # home directory
anyhow = "1"
tracing = "0.1"
thiserror = "1"
libc = "0.2" # Unix system calls (flock)
chrono = { version = "0.4", features = ["serde"] }
Expand Down
286 changes: 286 additions & 0 deletions docs/CONFIG_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ jcode --offline --provider-profile local-vllm
| `~/.jcode/gemini_oauth.json` | Gemini OAuth credentials | JSON |
| `~/.jcode/mcp.json` | Global MCP server registry | JSON |
| `.jcode/mcp.json` (project) | Project-local MCP servers | JSON |
| `~/.jcode/hooks.toml` | Hook configuration | TOML |
| `.jcode/hooks.toml` (project) | Project-level hooks | TOML |
| `~/.jcode/prompts/*.md` | User-level prompt templates | Markdown |
| `.jcode/prompts/*.md` (project) | Project-level prompt templates | Markdown |
| `~/.jcode/SYSTEM.md` | Global system-prompt override | Markdown |
Expand Down Expand Up @@ -191,6 +193,290 @@ For machines that cannot reach the public internet:
5. **`jcode doctor`**: runs without network access; use it to verify the
above before depending on jcode in production.

## Hooks

### Overview

Hooks allow you to intercept and react to events during jcode's execution lifecycle. They enable custom logic for logging, filtering, modifying tool inputs/outputs, enforcing policies, and integrating with external systems.

Hooks work by executing external commands (scripts, binaries, HTTP calls) that receive JSON context about the current event and return a response that can continue or block execution.

### Configuration

Hooks are configured in `hooks.toml` files at two levels:

| Path | Purpose | Priority |
|---|---|---|
| `~/.jcode/hooks.toml` | User-level hooks | Lower |
| `.jcode/hooks.toml` (project) | Project-level hooks | Higher (overrides user-level) |

The file format is TOML:

```toml
# Example: ~/.jcode/hooks.toml

[events.pre_tool_use]
command = "/usr/local/bin/my-hook-script.sh"
args = ["--verbose"]
env = { "HOOK_ENV" = "value" }
cwd = "/optional/working/dir"
timeout_secs = 30
pass_input_via_stdin = true

[events.post_tool_use]
command = "echo 'tool completed'"

[events.error]
command = "/usr/local/bin/error-handler.sh"

[events.custom:my_event]
command = "echo 'custom event triggered'"
```

### Events

| Event | Aliases | Description | Blocking |
|---|---|---|---|
| `PreToolUse` | `pretooluse`, `pre_tool_use` | Before a tool is executed | Yes |
| `PostToolUse` | `posttooluse`, `post_tool_use` | After a tool completes | No |
| `PreSession` | `presession`, `pre_session` | Before a session starts | Yes |
| `PostSession` | `postsession`, `post_session` | After a session ends | No |
| `Error` | `error` | On any error | No |
| `Custom:<name>` | — | Custom event (user-defined) | Depends |

**Event name parsing is case-insensitive.** Use any of the listed aliases in your config.

### Hook Input (JSON passed to hooks)

When a hook executes, it receives a JSON payload via stdin with the current context:

```json
{
"session_id": "sess_abc123",
"transcript_path": "/home/user/.jcode/sessions/sess_abc123.json",
"cwd": "/data/projects/myproject",
"hook_event_name": "PreToolUse",
"agent_id": null,
"agent_type": null,
"tool_name": "Bash",
"tool_input": { "command": "git status" },
"tool_use_id": "toolu_xyz789",
"permission_mode": null
}
```

**Available fields:**

| Field | Type | Description |
|---|---|---|
| `session_id` | String | Unique session identifier |
| `transcript_path` | String | Path to session transcript file |
| `cwd` | String | Current working directory |
| `hook_event_name` | String | Event that triggered this hook |
| `agent_id` | String? | Optional agent identifier |
| `agent_type` | String? | Optional agent type |
| `tool_name` | String? | Tool being executed (for tool events) |
| `tool_input` | JSON? | Tool input parameters |
| `tool_use_id` | String? | Unique tool use identifier |
| `permission_mode` | String? | Permission mode (if applicable) |

### Hook Output (JSON expected from hooks)

Hooks return a JSON response via stdout:

```json
{
"continue_": true,
"suppress_output": null,
"stop_reason": null,
"decision": null,
"reason": null,
"system_message": null,
"hook_specific_output": null
}
```

**Output fields:**

| Field | Type | Default | Description |
|---|---|---|---|
| `continue_` | bool | `true` | Whether to continue execution |
| `suppress_output` | bool? | null | Suppress tool output display |
| `stop_reason` | String? | null | Reason for stopping (if blocked) |
| `decision` | String? | null | Decision made by hook |
| `reason` | String? | null | Human-readable reason |
| `system_message` | String? | null | Message to inject into system |
| `hook_specific_output` | Object? | null | Event-specific fields (see below) |

**`hook_specific_output` fields:**

| Field | Type | Description |
|---|---|---|
| `hook_event_name` | String | Event name |
| `permission_decision` | String? | Allow/deny decision |
| `permission_decision_reason` | String? | Reason for permission decision |
| `updated_input` | JSON? | Modified tool input |
| `additional_context` | String? | Extra context to include |

**Blocking behavior:**
- Return `continue_: false` to block execution
- Exit code 2 also signals a block

### Handler Configuration

Each hook event in the config maps to a `HookHandlerConfig`:

```toml
[events.<event_name>]
command = "/path/to/handler" # Required: command to execute
args = ["arg1", "arg2"] # Optional: arguments (default: [])
env = { "KEY" = "value" } # Optional: environment variables
cwd = "/working/dir" # Optional: working directory
timeout_secs = 30 # Optional: execution timeout (default: 30s)
pass_input_via_stdin = true # Optional: send JSON input via stdin
```

### Matcher Types

Matchers control when a hook fires based on the tool name or context. Four matcher types are supported:

**1. Exact Match** — Matches a single tool name exactly:

```toml
[events.pre_tool_use]
# Handler only fires for Bash tool
command = "/hooks/bash-only.sh"
# (No matcher = matches all)
```

**2. Multi Match** — Matches any of several tools (pipe-separated):

```toml
[events.pre_tool_use]
command = "/hooks/write-edit.sh"
# Handler fires for Write OR Edit tools
```

**3. Regex Match** — Matches tool name via regex pattern:

```toml
[events.pre_tool_use]
command = "/hooks/bash-git.sh"
# Handler fires for Bash tools with git commands
# Context includes the full command for regex matching
```

**4. Wildcard** — Matches all events of this type:

```toml
[events.pre_tool_use]
command = "/hooks/log-all-tools.sh"
# Fires for every tool before execution
```

### Handler Types

Currently, only **Command** handlers are implemented.

**Command Handler** — Executes a shell command:

```toml
[events.pre_tool_use]
command = "/usr/local/bin/my-hook.sh"
args = ["--verbose", "--tool"]
env = { "SESSION_ID" = "123" }
cwd = "/tmp"
timeout_secs = 30
pass_input_via_stdin = true
```

The command receives the hook input as JSON via stdin and should output a `HookOutput` JSON response via stdout.

### Real-World Examples

**1. Log all tool executions:**

```toml
[events.pre_tool_use]
command = "logger"
args = ["tool_executed"]
env = { "LEVEL" = "INFO" }

[events.post_tool_use]
command = "logger"
args = ["tool_completed"]
env = { "LEVEL" = "INFO" }
```

**2. Block dangerous commands:**

```bash
#!/bin/bash
# /hooks/block-rm-rf.sh
read -r input
echo "$input" | jq -e '.tool_input.command | test("rm\\s+-rf\\s+/")' > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo '{"continue_": false, "stop_reason": "Dangerous rm -rf detected", "decision": "block"}'
exit 2
fi
echo '{"continue_": true}'
```

```toml
[events.pre_tool_use]
command = "/hooks/block-rm-rf.sh"
```

**3. Audit trail to file:**

```toml
[events.post_tool_use]
command = "tee"
args = ["-a", "/var/log/jcode-audit.jsonl"]
pass_input_via_stdin = true
```

**4. HTTP webhook notification:**

```bash
#!/bin/bash
# /hooks/webhook.sh
read -r input
curl -s -X POST "https://hooks.example.com/jcode" \
-H "Content-Type: application/json" \
-d "$input" > /dev/null
echo '{"continue_": true}'
```

```toml
[events.post_tool_use]
command = "/hooks/webhook.sh"
```

**5. Custom event for testing:**

```toml
[events.custom:test_event]
command = "echo 'test event fired'"
```

Trigger via the jcode API or internal events system that dispatches custom events.

### Conditionals

Hooks support simple `if_` conditions to filter when they execute:

```toml
[events.pre_tool_use.if_bash_destructive]
command = "/hooks/confirm-destructive.sh"
# Handler condition: tool_name=Bash
# Also checks tool_input.command for destructive patterns
```

Conditions are shell-like expressions: `field=value` or `field!=value`

Supported fields: `tool_name`, `agent_type`, `permission_mode`

## See also

- [Z.AI Coding Plan quickstart](ZAI_CODING_PLAN.md)
Expand Down
Loading