Hooks
A hook is a command of yours that runs around the tool calls the model makes. /hooks keeps them in the session:
/hooks add pre-tool node .orkeon/hooks/pre.mjs {tool} {path}
/hooks add post-tool node .orkeon/hooks/post.mjs {tool} {path}
/hooks list
/hooks remove 2
They last until /clear or the end of the REPL, like the permission rules.
Where they run
The dev steps /dev-spec, /dev-plan and /dev-batches hand the
registry to their crew, and so does /tasks repair to the agent it starts again. For each tool call the model makes in that crew, once the host gate has allowed it:
- the crew's callback checks the call again against the step's rule sets (and, for a role of a batch, against its roots); a call they refuse runs no hook;
- every
pre-toolhook runs, in the order of/hooks list; the first that exits with a code other than 0, runs longer than 60 seconds or cannot run refuses the call: the model reads the hook's number, its command, its exit code and the first line it printed (its error output first), and the call does not run; - the call runs;
- every
post-toolhook runs, in order. One that fails adds a line to the result the model reads ([post-tool hook #2 (node .orkeon/hooks/post.mjs file_write src/x.ts) exited 1: …]); when they all exit 0, the result is left as it is. A result that is not text reaches the model then as compact JSON, on one line.
The time a hook runs counts in the step's time budget, at each call: keep hooks short.
A call the gate refuses, a session deny rule among them, never reaches the hooks. The assistant (/assistant) does
not run them yet, and nothing runs a stop hook: /hooks stores them for later. Under orkeon run, a crew gets
the same hooks from its hooks input (--inputs '{"hooks":[{"event":"pre-tool","command":"…"}]}').
What a hook command is
Orkeon Code runs a hook with the shell tool, from its own code, in /workspace: the gate does not check it, and the
command is one program and its arguments, as the tool reads it. ;, &&, ||, |, &, <, >, >>, $( and
backticks are refused, by the tool and by /hooks add, and no variable is expanded. Put the steps in a script and
name the script:
// .orkeon/hooks/pre.mjs <tool> <path>: log every call, refuse writes under generated/ (relative or absolute).
import { appendFileSync } from "node:fs";
const [tool, path] = process.argv.slice(2);
appendFileSync(".orkeon/hooks/calls.log", `${new Date().toISOString()} ${tool} ${path}\n`);
if (path.startsWith("generated/") || path.startsWith("/workspace/generated/")) {
console.error("generated/ is rebuilt, not edited");
process.exit(2);
}
Three words are replaced in the command before it runs:
| Word | Replaced by |
|---|---|
{tool} |
the name of the tool the model called, file_write |
{event} |
pre-tool or post-tool |
{path} |
the call's path argument, as the model wrote it, when it holds only letters, digits and ._/@+=,:- and does not start with -; - otherwise or without one |
The program must be one the shell tool runs: node, npm, dotnet and find under Orkeon:Tools:Shell:AllowInterpreters,
which the example settings turn on, and any other under Orkeon:Tools:Shell:ExtraAllowedCommands ("bash" for a
shell script; Configuration). A hook that cannot run refuses every call of the step: run it once
by hand first (node .orkeon/hooks/pre.mjs file_read README.md).
What a hook does not do
It cannot allow a call the gate or the rule sets refuse, nor change the call's input. It cannot ask you: an approval asked in the middle of a crew's turn may not be answered, so a hook decides on its own. It does not see the calls a command or a crew makes from its own code (the git commands of a batch's audit, its build and tests), nor the inner calls of a tool Orkeon Code defines, which check their own paths.