Skip to content

Commit abd127e

Browse files
authored
Merge pull request #225 from editor-code-assistant/advanced-hooks
Advanced hooks
2 parents 8042060 + a0d5feb commit abd127e

File tree

18 files changed

+1966
-646
lines changed

18 files changed

+1966
-646
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## Unreleased
4+
- Enhanced hooks documentation with new types (sessionStart, sessionEnd, chatStart, chatEnd), JSON input/output schemas, execution options (timeout)
45

56
- Fix custom tools to support argument numbers.
67
- Improve read_file summary to mention offset being read.

docs/configuration.md

Lines changed: 132 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -349,51 +349,83 @@ ECA allows to totally customize the prompt sent to LLM via the `behavior` config
349349
}
350350
}
351351
```
352-
352+
353353
## Hooks
354354

355-
Hooks are actions that can run before or after an specific event, useful to notify after prompt finished or to block a tool call doing some check in a script.
355+
Hooks are shell actions that run before or after specific events, useful for notifications, injecting context, modifying inputs, or blocking tool calls.
356+
357+
### Hook Types
358+
359+
| Type | When | Can Modify |
360+
|------|------|------------|
361+
| `sessionStart` | Server initialized | - |
362+
| `sessionEnd` | Server shutting down | - |
363+
| `chatStart` | New chat or resumed chat | Can inject `additionalContext` |
364+
| `chatEnd` | Chat deleted | - |
365+
| `preRequest` | Before prompt sent to LLM | Can rewrite prompt, inject context, stop request |
366+
| `postRequest` | After prompt finished | - |
367+
| `preToolCall` | Before tool execution | Can modify args, override approval, reject |
368+
| `postToolCall` | After tool execution | Can inject context for next LLM turn |
369+
370+
### Hook Options
356371

357-
Allowed hook types:
372+
- **`matcher`**: Regex for `server__tool-name`, only for `*ToolCall` hooks.
373+
- **`visible`**: Show hook execution in chat (default: `true`).
374+
- **`runOnError`**: For `postToolCall`, run even if tool errored (default: `false`).
358375

359-
- `preRequest`: Run before prompt is sent to LLM, if a hook output is provided, append to user prompt.
360-
- `postRequest`: Run after prompt is finished, when chat come back to idle state.
361-
- `preToolCall`: Run before a tool is called, if a hook exit with status `2`, reject the tool call.
362-
- `postToolCall`: Run after a tool was called.
376+
### Execution Details
363377

364-
__Input__: Hooks will receive input as json with information from that event, like tool name, args or user prompt.
378+
- **Order**: Alphabetical by key. Prompt rewrites chain; argument updates merge (last wins).
379+
- **Conflict**: Any rejection (`deny` or exit `2`) blocks the call immediately.
380+
- **Timeout**: Actions time out after 30s unless `"timeout": ms` is set.
365381

366-
__Output__: All hook actions allow printing output (stdout) and errors (stderr) which will be shown in chat.
382+
### Input / Output
367383

368-
__Matcher__: Specify whether to apply this hook checking a regex applying to `mcp__tool-name`, applicable only for `*ToolCall` hooks.
384+
Hooks receive JSON via stdin with event data (top-level keys `snake_case`, nested data preserves case). Common fields:
369385

370-
__Visible__: whether to show or not this hook in chat when executing, defaults to true.
386+
- All hooks: `hook_name`, `hook_type`, `workspaces`, `db_cache_path`
387+
- Chat hooks add: `chat_id`, `behavior`
388+
- Tool hooks add: `tool_name`, `server`, `tool_input`, `approval` (pre) or `tool_response`, `error` (post)
389+
- `chatStart` adds: `resumed` (boolean)
371390

372-
Examples:
391+
Hooks can output JSON to control behavior:
392+
393+
```javascript
394+
{
395+
"additionalContext": "Extra context for LLM", // injected as XML block
396+
"replacedPrompt": "New prompt text", // preRequest only
397+
"updatedInput": {"key": "value"}, // preToolCall: merge into tool args
398+
"approval": "allow" | "ask" | "deny", // preToolCall: override approval
399+
"continue": false, // stop processing (with optional stopReason)
400+
"stopReason": "Why stopped",
401+
"suppressOutput": true // hide hook output from chat
402+
}
403+
```
373404

374-
=== "Notify after prompt finish"
405+
Plain text output (non-JSON) is treated as `additionalContext`.
406+
407+
To reject a tool call, either output `{"approval": "deny"}` or exit with code `2`.
408+
409+
### Examples
410+
411+
=== "Notify after prompt"
375412

376413
```javascript title="~/.config/eca/config.json"
377414
{
378415
"hooks": {
379416
"notify-me": {
380417
"type": "postRequest",
381418
"visible": false,
382-
"actions": [
383-
{
384-
"type": "shell",
385-
"shell": "notify-send \"Hey, prompt finished!\""
386-
}
387-
]
419+
"actions": [{"type": "shell", "shell": "notify-send 'Prompt finished!'"}]
388420
}
389421
}
390-
}
422+
}
391423
```
392-
424+
393425
=== "Ring bell sound when pending tool call approval"
394426

395427
```javascript title="~/.config/eca/hooks/my-hook.sh"
396-
[[ $(jq '.approval == "ask"' <<< "$1") ]] && canberra-gtk-play -i complete
428+
jq -e '.approval == "ask"' > /dev/null && canberra-gtk-play -i complete
397429
```
398430

399431
```javascript title="~/.config/eca/config.json"
@@ -405,34 +437,93 @@ Examples:
405437
"actions": [
406438
{
407439
"type": "shell",
408-
"shell": "${file:hooks/my-hook.sh}"
440+
"file": "hooks/my-hook.sh"
409441
}
410442
]
411443
}
412444
}
413445
}
414446
```
415447

448+
=== "Inject context on chat start"
449+
450+
```javascript title="~/.config/eca/config.json"
451+
{
452+
"hooks": {
453+
"load-context": {
454+
"type": "chatStart",
455+
"actions": [{
456+
"type": "shell",
457+
"shell": "echo '{\"additionalContext\": \"Today is '$(date +%Y-%m-%d)'\"}'"
458+
}]
459+
}
460+
}
461+
}
462+
```
416463

417-
=== "Block specific tool call checking hook arg"
464+
=== "Rewrite prompt"
418465

419466
```javascript title="~/.config/eca/config.json"
420467
{
421468
"hooks": {
422-
"check-my-tool": {
423-
"type": "preToolCall",
424-
"matcher": "my-mcp__some-tool",
425-
"actions": [
426-
{
427-
"type": "shell",
428-
"shell": "tool=$(jq '.\"tool-name\"' <<< \"$1\"); echo \"We should not run the $tool tool bro!\" >&2 && exit 2"
429-
}
430-
]
469+
"add-prefix": {
470+
"type": "preRequest",
471+
"actions": [{
472+
"type": "shell",
473+
"shell": "jq -c '{replacedPrompt: (\"[IMPORTANT] \" + .prompt)}'"
474+
}]
431475
}
432476
}
433477
}
434478
```
435-
479+
480+
=== "Block tool with JSON response"
481+
482+
```javascript title="~/.config/eca/config.json"
483+
{
484+
"hooks": {
485+
"block-rm": {
486+
"type": "preToolCall",
487+
"matcher": "eca__shell_command",
488+
"actions": [{
489+
"type": "shell",
490+
"shell": "if jq -e '.tool_input.command | test(\"rm -rf\")' > /dev/null; then echo '{\"approval\":\"deny\",\"additionalContext\":\"Dangerous command blocked\"}'; fi"
491+
}]
492+
}
493+
}
494+
}
495+
```
496+
497+
=== "Modify tool arguments"
498+
499+
```javascript title="~/.config/eca/config.json"
500+
{
501+
"hooks": {
502+
"force-recursive": {
503+
"type": "preToolCall",
504+
"matcher": "eca__directory_tree",
505+
"actions": [{
506+
"type": "shell",
507+
"shell": "echo '{\"updatedInput\": {\"max_depth\": 3}}'"
508+
}]
509+
}
510+
}
511+
}
512+
```
513+
514+
=== "Use external script file"
515+
516+
```javascript title="~/.config/eca/config.json"
517+
{
518+
"hooks": {
519+
"my-hook": {
520+
"type": "preToolCall",
521+
"actions": [{"type": "shell", "file": "~/.config/eca/hooks/check-tool.sh"}]
522+
}
523+
}
524+
}
525+
```
526+
436527
## Completion
437528

438529
You can configure which model and system prompt ECA will use during its inline completion:
@@ -498,12 +589,16 @@ To configure, add your OTLP collector config via `:otlp` map following [otlp aut
498589
}};
499590
defaultModel?: string;
500591
hooks?: {[key: string]: {
501-
type: 'preToolCall' | 'postToolCall' | 'preRequest' | 'postRequest';
502-
matcher: string;
592+
type: 'sessionStart' | 'sessionEnd' | 'chatStart' | 'chatEnd' |
593+
'preRequest' | 'postRequest' | 'preToolCall' | 'postToolCall';
594+
matcher?: string; // regex for server__tool-name, only *ToolCall hooks
503595
visible?: boolean;
596+
runOnError?: boolean; // postToolCall only
504597
actions: {
505598
type: 'shell';
506-
shell: string;
599+
shell?: string; // inline script
600+
file?: string; // path to script file
601+
timeout?: number; // ms, default 30000
507602
}[];
508603
};
509604
};

0 commit comments

Comments
 (0)