There's a particular kind of panic you only feel once. For me it was watching an agent — mine, that I trusted — decide the cleanest way to resolve a merge conflict was git reset --hard followed by a force push. It asked for no confirmation because I'd given it none to ask for. I caught it. I might not have.
The lesson wasn't "don't use agents." It was that a system prompt is not a security boundary. "Please don't delete things" is a suggestion, and an agent under pressure to complete a task will talk itself past a suggestion. If you want an agent to be unable to do something, you have to make it actually unable — not ask nicely.
Octomind gives you three real boundaries, in increasing strength: scoped permissions, a filesystem sandbox, and guardrails. Use all three. None of them rely on the model's good behavior.
Boundary 1: Don't Hand Out Tools You Don't Need
The first principle is least privilege, and in Octomind it's a property of the role. A role declares which MCP servers it can reach (server_refs) and which specific tools within them it can call (allowed_tools). The model can only call what the role grants — full stop.
A read-only analyst that cannot modify your files:
[[roles]]
name = "analyst"
temperature = 0.2
top_p = 0.7
top_k = 20
welcome = "Analyst role ready (read-only)."
system = "You analyze code and provide insights. Do not modify files."
[roles.mcp]
server_refs = ["filesystem"]
allowed_tools = ["filesystem:view"]Grant filesystem:view and nothing else, and there is no text_editor, no shell, no batch_edit available to call. The prompt says "do not modify files," but the prompt isn't what's stopping it — the permission set is. That's the difference that matters.
The pattern language is precise: "server:*" for everything from a server, "server:tool_name" for one tool, "server:prefix_*" for a prefix match. So you can hand an agent filesystem:view and filesystem:shell but withhold the editor, or grant the editor but withhold shell. Scope each role to exactly the job it does. The Roles and Permissions doc has the full matrix.
This is also why delegating to sub-agents is a security move, not just an organizational one: a read-only reviewer agent cannot "helpfully" rewrite your code while it's reviewing.
Boundary 2: Sandbox the Filesystem
Scoped tools control which operations exist. The sandbox controls where they can reach. Add one flag:
octomind run --sandboxWith the sandbox on, all filesystem writes are restricted to the current working directory — enforced by the operating system (Landlock on Linux, Seatbelt on macOS), not by the agent's cooperation. The agent can read broadly but can only write inside your project. Nothing lands in ~/.ssh, nothing overwrites system files, no surprise writes three directories up. (Reads stay open — blocking those is what guards are for, below.)
I run --sandbox by default on anything I haven't personally vetted: a new tap, an experimental role, any unattended job. You can also set it permanently in config (sandbox = true). It costs nothing and removes an entire category of "wait, why did it write there" incidents. It applies to run, server, and acp.
Boundary 3: Guardrails — Block the Specific Thing
Permissions and sandboxing are coarse: a tool is allowed or it isn't, a path is writable or it isn't. But often you want something sharper — "shell is allowed, but never rm -rf," or "never read .env files," or "don't let it say it's done until tests have run." That's what guardrails are for.
Guardrails live in .agents/guardrails.toml in your project, and they cover four phases of a turn. Two of them can actually stop things:
-
[[pipe]]transforms or validates your input before the model ever sees it. -
[[guard]]blocks a tool call before it runs. -
[[hook]]reacts to a tool result (can nudge, can't undo). -
[[validator]]checks the assistant's final message (can nudge, can't undo).
The one that would have saved me is [[guard]]. A tiered shell policy looks like this:
[[guard]]
match = "shell(command=^rm\\s+-rf?\\s+/)"
message = "Refusing rm -rf on root paths."
[[guard]]
match = "shell(command=git push.*(--force|-f)\\b)"
message = "Force push blocked. Use --force-with-lease and ask first."
[[guard]]
match = "filesystem-read(paths=\\.env)"
message = "Refusing to read .env files."When a guard matches, the call is blocked before the executor runs — the model gets a synthetic error ([guardrail] Force push blocked...) and has to find another way. The destructive command never executes. This isn't the model choosing to comply; it's the call being intercepted on its way to the shell.
The matching language is a small DSL: capability(arg=regex) matches a tool call by capability and argument. You can also gate rules on history with when — for example, block ls only if the agent hasn't used the proper read tool yet:
[[guard]]
match = "shell(command=^ls\\b)"
has = "filesystem"
when = ["-filesystem-read"]
message = "Use the view tool instead of ls."Here has additionally requires the filesystem MCP server to be loaded for the role. Note the two namespaces: has takes a server name, while match and when take capability names (shell, filesystem-read) — the tap-declared capability that owns the tool.
Nudges: When You Want a Reminder, Not a Wall
Not every rule should be a hard block — a wrong [[guard]] blocks real work and gets in your way. For "I want the agent to do X before declaring victory," use a [[validator]], which runs at the end of a turn and pushes a message back to the agent if the rule fails:
[[validator]]
name = "test-before-done"
match = "(?i)\\b(done|finished|completed)\\b"
when = ["+filesystem-write", "-shell(command=cargo test)"]
script = ".agents/validators/remind-tests.sh"This fires when the agent says "done" and it edited files and it didn't run the tests — and injects "you edited files but didn't run the tests; run them before declaring done." The agent reads it on the next turn and goes back to work. It can't undo anything (validators only nudge), but it closes the gap between "the model thinks it's finished" and "it's actually finished."
A good rule of thumb while you're iterating: prefer validators over guards. A wrong validator just nags; a wrong guard stops you. Start permissive, watch what the agent actually does, and promote a nag to a block only once you're sure. The full four-phase system — the matching DSL, the call-log history model, execution order — is documented in Guardrails, and the bigger argument for why prompts alone don't cut it is in Guardrails for AI Agents.
Layer Them
These aren't alternatives; they stack. Scope the role so dangerous tools aren't even present. Sandbox the filesystem so writes can't escape the project. Add guardrails for the specific commands that are allowed-but-risky. Each layer catches what the one above it lets through, and none of them depend on the agent deciding to behave.
If you're thinking about this seriously, the OWASP Top 10 for AI agents is the threat model worth mapping your guardrails against — excessive agency and unsafe tool use are right at the top of the list, and these three boundaries are how you answer them.
The agent I almost let force-push? It runs sandboxed now, with a guard on git push --force and a role that asks before anything destructive. Same agent, same model. The difference is that "don't do the scary thing" stopped being a request and became a wall.
Get Octomind — and give your agent exactly the power it needs. No more.



