bots-garden/mini-mepublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 5266466 · on main · k33g · 14h ago
triage-commands-with-a-decision-model.md · 85 lines · 4.1 KBmarkdown
Blame HistoryOpen raw

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

  1. 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.
    
  2. Write a preToolCall hook that asks the decider and answers allow when 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 deny for a command judged dangerous and destructive.

  3. 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
    
  4. 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 with yolo: false every 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) and TRIAGE_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 timeout the hook fails, onError: warn reports it, and you are asked as usual.
  • mm is not on the PATH: set MM_BIN to the binary's path in the demo's hook.
  • Every command asks, even ls: the hook runs the decider in your project's directory. With skillsDir: "" the decider reads ./skills there, declares read_skill, and refuses to start — so the hook has no verdict. Give the decider skillsDir: no-skills.

See also

 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
# 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](ask-a-decision-model.md)) and that you run with `yolo: false` (see [How to confirm tool calls](confirm-tool-calls.md)). The finished files are in `demos/decision-commands/`.

## Steps

1. Give the decider two questions about a command, in its `decision:` block:

   ```yaml
   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.
   ```

2. Write a `preToolCall` hook that asks the decider and answers `allow` when it is sure. In Node.js, `hooks/triage.js`:

   ```javascript
   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 `deny` for a command judged dangerous **and** destructive.

3. Declare the hook in the agent that answers you:

   ```yaml
   yolo: false
   displayHooks: true
   hooks:
     preToolCall:
       - matcher: ^bash$
         command: node "$MM_CONFIG_DIR/hooks/triage.js"
         timeout: 20s
   ```

4. 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 with `yolo: false` every 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) and `TRIAGE_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 `timeout` the hook fails, `onError: warn` reports it, and you are asked as usual.
- **mm is not on the PATH**: set `MM_BIN` to the binary's path in the demo's hook.
- **Every command asks, even `ls`**: the hook runs the decider in your project's directory. With `skillsDir: ""` the decider reads `./skills` there, declares `read_skill`, and refuses to start — so the hook has no verdict. Give the decider `skillsDir: no-skills`.

## See also

- The `allow` action, and what it never overrules: [Hooks reference](../reference/hooks.md#pretoolcall)
- Why the decider only ever removes questions, never adds permissions: [Decision models](../explanation/decision-models.md#triage-only-removes-questions)