5.5

permissions, --safety, and hooks

Deny, allow, and ask patterns, then a safety mode, then hooks decide whether a tool call runs, and the docs say none of them is a security boundary.

Your agent runs in CI with --exec, and nobody watches the terminal. You want echo to run, rm never to run, and every other call to fail closed unless its label is safe. The agent file and one flag can say that, but only for calls that go through docker-agent.

When you finish this section, you can write permissions patterns, pick a --safety mode for an unattended run, and predict what a pre_tool_use hook changes.

The same page says that the restricted mode is an extra layer against unwanted tool calls, not a security boundary. For isolation, run the agent in a sandbox, as docker-agent run --sandbox shows.

The guarded agent

one shell toolset, a hook, and two patternsyamlFull log
version: "16"

agents:
  root:
    model: local
    description: Runs shell commands under a permission list and a hook.
    instruction: |
      Run exactly the shell commands the user lists, one tool call per
      command, in the given order. Then report each command and its
      result or refusal in one line each.
    max_iterations: 8
    toolsets:
      - type: shell
    hooks:
      pre_tool_use:
        - matcher: shell
          preempt_yolo: true
          hooks:
            - type: command
              command: ./fixtures/hooks/log-hook.sh
              env:
                M101_HOOK_LOG: ./work/hook-stdin.jsonl

permissions:
  allow:
    - "shell:cmd=echo*"
  deny:
    - "shell:cmd=rm*"
…
The models block is cut.

Permission pattern: a tool name glob with optional argument conditions, such as shell:cmd=rm*, in an allow, ask, or deny list. Patterns from the agent file and from settings.permissions in the user config merge, and a deny from either side wins.

Safety mode: what the runtime does with a call that no pattern matched. It reads the call's label, safe, destructive, or unknown, and the mode decides:

Modesafedestructiveunknown
strictaskaskask
balancedallowaskask
restrictedallowdenydeny
autonomousallowallowallow

--yolo is the same as --safety autonomous. A session that never chooses a mode keeps the old default: read-only tools run without a prompt, and every other tool asks. That is why the wc -l call in run, new, and doctor asked although its label was safe, because shell carries readOnlyHint: false.

Three calls in two modes

The kit sent the same three messages under --safety strict and --safety restricted, one command per message, with no terminal to answer a prompt.

CommandWhat decidedstrictrestricted
echo m101-okallow: shell:cmd=echo*ran, m101-okran, m101-ok
pwdno pattern, label safeasked, then "The user rejected the tool call."ran, $CAPTURE
rm -rf work/m101-nothingdeny: shell:cmd=rm*"Tool 'shell' is denied by permissions configuration."the same denial

The patterns behaved the same in both modes, and only the unmatched pwd changed. Under restricted, the mode allowed the safe label. Under strict, the runtime raised tool_call_confirmation, and the empty terminal turned it into a rejection (20-strict.txt, 20-restricted.txt).

The order, and what a hook can change

The docs give one order for every call:

  1. preempt_yolo pre_tool_use hooks run first, and no mode or allow rule can bypass their deny or ask.
  2. A deny pattern blocks the call.
  3. An allow pattern approves it.
  4. An ask pattern prompts the user.
  5. With no match, the safety mode applies to the call's label.
  6. On a mode ask, default pre_tool_use hooks can allow, deny, or ask.
  7. With no decision, the user is asked.

Hook: a command, builtin, model, or evaluator entry that runs at a named event. A command hook reads one JSON object on stdin and can answer with JSON on stdout. Exit code 2 blocks, and the default timeout is 60 seconds.

what the hook received for the rm calljsonlFull log
{"agent_name": "root", "cwd": "$CAPTURE", "hook_event_name": "pre_tool_use", "safety_policy": "restricted", "session_id": "<uuid>", "tool_input": {"cmd": "rm -rf work/m101-nothing"}, "tool_name": "shell", "tool_use_id": "<call-id>"}
The third of three lines, one per call of the restricted run.

log-hook.sh appends that line to a file and prints "permission_decision":"allow". The stream shows it as pre_tool_use_pre_yolo with "allowed": true on all three calls, yet rm was denied and strict still asked for pwd. From a preempting hook, an allow verdict is only advice. Its deny or ask would have ended the call.

Two more hooks run on every call: tool_input_transform and tool_response_transform appear in each stream, although no file declares them. redact_secrets, true by default, installs a builtin on those events. A hook can also run several times at once. When one message asked for three commands, the hook ran three times at once. Two runs appended to the log file together (kit notes). Write each record in one write.

Fig. 5.5three shell calls through the approval orderdecision
Three shell calls through the approval order The approval order for the shell calls of guarded.yaml, top down. The preempt_yolo hook log-hook.sh runs on every call and returns allow, which is advice only. A deny pattern shell:cmd=rm* blocks rm -rf work/m101-nothing with Tool shell is denied by permissions configuration. An allow pattern shell:cmd=echo* runs echo m101-ok in both modes. The file has no ask patterns. The safety mode then decides pwd, which has the label safe: restricted runs it and prints the capture directory, and strict asks the user, which under exec with no terminal ends as The user rejected the tool call. SHELL CALLS OF GUARDED.YAML, --SAFETY STRICT AND RESTRICTED 20-STRICT.NDJSON, 20-RESTRICTED.NDJSON preempt_yolo hook log-hook.sh, on every call allow, as advice only pre_tool_use_pre_yolo, allowed: true a deny or ask here would end the call go on a deny pattern matches? shell:cmd=rm* rm -rf work/m101-nothing Tool 'shell' is denied by permissions configuration. no an allow pattern matches? shell:cmd=echo* echo m101-ok runs result m101-ok in both modes no an ask pattern matches? guarded.yaml has none no safety mode on the label pwd has the label safe restricted: pwd runs result $CAPTURE strict strict: ask the user default hooks first, none here no terminal under --exec The user rejected the tool call.
The preempting hook allows every call only as advice, the patterns decide rm and echo, and the safety mode alone decides pwd. Read top down. Each question box is one stage of the order, and the box to its right is what the capture saw at that stage. Rose is a denial or rejection, and olive a command that ran.

Docker Sandboxes (sbx) and Docker Agent (docker-agent) sbx 0.47.0, docker-agent 1.149.0 · bf4169c · 2026-10-07 · Edition 2026.10