How to triage commands with a decision model
This guide shows how to let a decision model answer the confirmation question for you when a bash command clearly only reads: the command runs without asking, and every other command asks as before. It assumes you have a decider agent file (see How to ask a decision model) and that you run with yolo: false (see How to confirm tool calls). The finished files are in demos/decision-commands/.
Steps
-
Give the decider two questions about a command, in its
decision:block:decision: timeout: 60s questions: risk: type: choice instructions: How risky is running this shell command in the project directory? criteria: read-only: Only reads files or prints information local-change: Changes files inside the project, easy to rebuild or revert dangerous: Deletes or changes things outside the project, uses sudo, the network, git push or force destructive: type: noul instructions: The command deletes data that cannot be rebuilt. -
Write a
preToolCallhook that asks the decider and answersallowwhen it is sure. In Node.js,hooks/triage.js:const { execFileSync } = require("child_process"); const path = require("path"); const input = JSON.parse(require("fs").readFileSync(0, "utf8")); const command = input.tool.input.command || ""; let a; try { const out = execFileSync("mm", ["-config", path.join(process.env.MM_CONFIG_DIR, "agent.decider.yaml"), "-p", `Command the agent wants to run in ${input.cwd}: ${command}`], { encoding: "utf8" }); a = JSON.parse(out).answers; } catch { process.exit(0); // no verdict: the user is asked } if (a.risk.choice === "read-only" && a.risk.confidence >= 0.8 && a.destructive.noul < 0.3) { console.log(JSON.stringify({ action: "allow" })); }Printing nothing leaves the decision to you. The demo's hook also answers
denyfor a command judged dangerous and destructive. -
Declare the hook in the agent that answers you:
yolo: false displayHooks: true hooks: preToolCall: - matcher: ^bash$ command: node "$MM_CONFIG_DIR/hooks/triage.js" timeout: 20s -
Ask for something that reads, then something that changes. The first command runs without a question, the second asks:
> show the content of notes.txt, then delete the build folder 🛠️ bash: cat notes.txt │ notes about the playground 🛠️ bash: rm -rf build 🔐 bash: rm -rf build [a] allow once · [s] allow bash for this session · [r] reject · [n] never bash ?With
displayHooks: true, the list after the answer says what the hook decided for each call:preToolCall bash · node "$MM_CONFIG_DIR/hooks/triage.js" → allow, then→ continue.
Variants
- In a script (
mm -p): nobody can be asked there, so withyolo: falseevery call that asks is rejected. A call the hook allowed runs. Use this to let a scripted agent read freely and change nothing. - Stricter or looser: the demo's thresholds are environment variables,
TRIAGE_ALLOW_CONFIDENCE(0.8) andTRIAGE_ALLOW_DESTRUCTIVE(0.3). Measure on your own commands before you loosen them. - A cold decider: the first call loads the model (2 to 13 s). Past the hook's
timeoutthe hook fails,onError: warnreports it, and you are asked as usual. - mm is not on the PATH: set
MM_BINto the binary's path in the demo's hook. - Every command asks, even
ls: the hook runs the decider in your project's directory. WithskillsDir: ""the decider reads./skillsthere, declaresread_skill, and refuses to start — so the hook has no verdict. Give the deciderskillsDir: no-skills.
See also
- The
allowaction, and what it never overrules: Hooks reference - Why the decider only ever removes questions, never adds permissions: Decision models
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
|