|
| 1 | +# Native lifecycle hooks — 0.20 development |
| 2 | + |
| 3 | +Native hooks run explicitly enabled project commands at fixed points in an |
| 4 | +agent task. They use the Rust process runner and need no Python interpreter or |
| 5 | +callback loader. This guide covers the native development branch; the |
| 6 | +[remaining release gates](NATIVE_MIGRATION.md) still apply. |
| 7 | + |
| 8 | +## Define and enable a check |
| 9 | + |
| 10 | +Create `.shadowcode/hooks/check.yaml` in your project: |
| 11 | + |
| 12 | +```yaml |
| 13 | +name: check |
| 14 | +description: Run the Rust test suite before reporting success. |
| 15 | +events: [on_complete] |
| 16 | +command: cargo test --locked |
| 17 | +timeout_sec: 60 |
| 18 | +``` |
| 19 | +
|
| 20 | +Open **Settings → Hooks**, select **Refresh hooks**, inspect the command, and |
| 21 | +choose **Enable check**. Project trust is required. Discovery only reads data; |
| 22 | +opening a repository or listing its hooks does not execute them. |
| 23 | +
|
| 24 | +The CLI exposes the same catalog and activation: |
| 25 | +
|
| 26 | +```sh |
| 27 | +shadowcode --workspace /path/to/project --json hooks |
| 28 | +shadowcode --workspace /path/to/project hooks \ |
| 29 | + --enable .shadowcode/hooks/check.yaml --hash HASH_FROM_THE_CATALOG |
| 30 | +shadowcode --workspace /path/to/project hooks --disable .shadowcode/hooks/check.yaml |
| 31 | +``` |
| 32 | + |
| 33 | +Use your development `--profile` consistently. The path and SHA-256 hash identify |
| 34 | +the definition you reviewed. If its contents change, refresh and review it again. |
| 35 | +An enabled definition that changes or disappears blocks new Build tasks until |
| 36 | +you approve the current contents or disable the old registration. Settings keeps |
| 37 | +unavailable registrations visible so they can be removed. Plan and Review stay |
| 38 | +usable with these hooks inactive. |
| 39 | + |
| 40 | +## Definition format and limits |
| 41 | + |
| 42 | +Definitions are direct `.yaml`, `.yml`, or `.json` files in `.shadowcode/hooks` |
| 43 | +or `.shadow/hooks`. Both the filename stem and `name` accept 1–80 ASCII letters, |
| 44 | +digits, underscores and hyphens. Symlink escapes and unsupported fields are |
| 45 | +rejected. Required fields are `name`, `events`, and a nonempty `command`. |
| 46 | + |
| 47 | +Optional `description` describes the check. `timeout_sec` defaults to 30 and |
| 48 | +accepts 1–120 seconds, capped further by the task's configured tool timeout. |
| 49 | +`path_suffix` restricts execution to events with a matching affected path; |
| 50 | +it is useful for an `after_edit` formatter: |
| 51 | + |
| 52 | +```yaml |
| 53 | +name: format-rust |
| 54 | +events: [after_edit] |
| 55 | +path_suffix: .rs |
| 56 | +command: rustfmt -- "$SHADOW_HOOK_PATH" |
| 57 | +timeout_sec: 10 |
| 58 | +``` |
| 59 | +
|
| 60 | +Definitions are limited to 32 KB, commands to 8 KB, descriptions to 1 KB, and |
| 61 | +path suffixes to 80 bytes. The catalog displays at most 64 definitions/issues; |
| 62 | +at most 16 hooks can be enabled per project and 256 registrations per profile. |
| 63 | +Commands run sequentially in definition-path order. Output capture is limited |
| 64 | +to 16 KB per invocation while both streams continue draining. |
| 65 | +
|
| 66 | +## Events and failure behavior |
| 67 | +
|
| 68 | +- `before_command`: before an agent `exec` action, after its normal permission |
| 69 | + decision. A failed check blocks the action and later checks for that event. |
| 70 | +- `before_commit`: before the agent's `git_commit` tool, with the same blocking |
| 71 | + behavior. It does not intercept arbitrary Git commands in a shell. |
| 72 | +- `after_edit`: after a successful `write_file`, `edit_file`, or `apply_patch`. |
| 73 | + A patch fires once for each changed path, after all patch changes are applied. |
| 74 | + Failure reports a failed tool result; it does not undo the changes. |
| 75 | +- `after_test`: after an agent `exec` that resembles a test command, including |
| 76 | + a failed test. Detection recognizes common test runner words and commands such |
| 77 | + as `cargo test` and `npm test`; it is a lexical heuristic, not a shell parser. |
| 78 | + It can miss aliases or match words in an `echo` command. Use `on_complete` for |
| 79 | + a required check rather than depending on test detection. |
| 80 | +- `on_error`: after a failed agent tool or a final provider failure after its |
| 81 | + retries. Hook failures do not recursively invoke error hooks. This event does |
| 82 | + not promise to catch every validation, startup, storage, or cancellation error. |
| 83 | +- `on_complete`: before an agent task can finish successfully. A failed check |
| 84 | + returns its recorded output to the model for repair, bounded by |
| 85 | + `agent.max_fix_retries` and the task's normal step/token limits. Exhaustion |
| 86 | + leaves the task failed. A repaired completion runs the checks again. |
| 87 | + Repair context uses an 8 KB diagnostic excerpt across failed checks so noisy |
| 88 | + commands do not fill the model's next request; full captured output stays in |
| 89 | + task history. |
| 90 | + Ollama requests include runtime guidance in the leading system block because |
| 91 | + some model templates ignore later system messages; notes retain their original |
| 92 | + conversation positions and persisted history is unchanged. |
| 93 | +- `on_compaction`: after context history is compacted. Failure stops the task. |
| 94 | + |
| 95 | +Hooks belong to agent tasks. Manual terminal commands, background processes, |
| 96 | +UI Git actions, and commands launched inside another hook do not invoke them. |
| 97 | +They are not an operating-system policy or repository-wide Git hook mechanism. |
| 98 | + |
| 99 | +Each invocation records `hook.started` and `hook.completed` events with its |
| 100 | +definition path/hash, command, status and process result. The conversation shows |
| 101 | +a durable hook card; the CLI reports hook outcomes and `run --events` includes |
| 102 | +the saved events. A running card means execution has started; output is saved |
| 103 | +at completion, rather than streamed into that card. |
| 104 | + |
| 105 | +## Context, cancellation and trust |
| 106 | + |
| 107 | +Commands run under `/bin/sh -c` with the project as working directory. The |
| 108 | +environment includes `SHADOW_HOOK_EVENT`, `SHADOW_HOOK_PATH` (one affected file), |
| 109 | +`SHADOW_HOOK_COMMAND` (the original agent command), `SHADOW_HOOK_TASK_ID`, and |
| 110 | +`SHADOW_HOOK_SESSION_ID`. `SHADOW_HOOK_CONTEXT` contains JSON with `event`, `tool`, |
| 111 | +`paths`, `command`, `exit_code`, `detail`, and `detail_truncated`. |
| 112 | +Detail is capped at 4 KB. Operational fields remain exact; context exceeding |
| 113 | +64 KB fails the check instead of supplying a shortened command to a gate. |
| 114 | + |
| 115 | +Context is passed as environment data. Quote its variables and never `eval` |
| 116 | +them. It can contain untrusted filenames, commands or model output. A filename |
| 117 | +containing shell syntax is a literal path when used as `"$SHADOW_HOOK_PATH"`. |
| 118 | + |
| 119 | +Enabling a hook is standing approval to run that command in this project. |
| 120 | +The SHA-256 approval pins the definition, **not the contents of scripts, |
| 121 | +executables or dependencies it invokes**. Review those dependencies as part of |
| 122 | +trusting the command. Hooks inherit the user's environment and filesystem |
| 123 | +access, can change files beyond the project, and are not sandboxed. Existing |
| 124 | +root/network command restrictions are still checked, but those lexical checks |
| 125 | +cannot constrain arbitrary script behavior. No hook runs in an untrusted |
| 126 | +project or a read-only task. |
| 127 | + |
| 128 | +Settings changes apply to new tasks. Active and queued tasks retain their |
| 129 | +configuration snapshot; disable does not interrupt one already in progress. |
| 130 | +Cancel the task to stop it. The manifest hash is rechecked before every |
| 131 | +invocation, including queued work, so an edited definition is never silently |
| 132 | +executed with old approval. Cancellation, timeout and engine shutdown stop the |
| 133 | +owned process group and wait for cleanup before the task releases its workspace. |
| 134 | + |
| 135 | +Hook shell edits are not native file-tool checkpoints. A formatter can therefore |
| 136 | +cause a later rewind conflict; checkpoint recovery preserves those changes |
| 137 | +rather than overwriting them. File observations are invalidated after a hook |
| 138 | +runs so subsequent full-file writes must read current contents again. Agent |
| 139 | +reads run serially while hooks are active because even an error check can change |
| 140 | +the workspace. |
| 141 | + |
| 142 | +## Migrating Python callbacks |
| 143 | + |
| 144 | +Legacy `*.py` files exporting `register(registry)` appear as inactive migration |
| 145 | +issues. Native ShadowCode does not import them or automatically translate them. |
| 146 | +Move formatting/check commands into reviewed YAML definitions. There is no |
| 147 | +automatic built-in formatter or Python test callback in the native engine. |
| 148 | +External tools such as `rustfmt`, `npm`, or `pytest` must be installed by the |
| 149 | +project owner; choosing a Python command requires that external interpreter, |
| 150 | +but the application itself still has no Python runtime dependency. |
0 commit comments