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
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*"
…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:
| Mode | safe | destructive | unknown |
|---|---|---|---|
strict | ask | ask | ask |
balanced | allow | ask | ask |
restricted | allow | deny | deny |
autonomous | allow | allow | allow |
--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.
| Command | What decided | strict | restricted |
|---|---|---|---|
echo m101-ok | allow: shell:cmd=echo* | ran, m101-ok | ran, m101-ok |
pwd | no pattern, label safe | asked, then "The user rejected the tool call." | ran, $CAPTURE |
rm -rf work/m101-nothing | deny: 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:
preempt_yolopre_tool_usehooks run first, and no mode or allow rule can bypass their deny or ask.- A
denypattern blocks the call. - An
allowpattern approves it. - An
askpattern prompts the user. - With no match, the safety mode applies to the call's label.
- On a mode ask, default
pre_tool_usehooks can allow, deny, or ask. - 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.
{"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>"}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.
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