Safety & Gating

How shell and tool calls are vetted before they run

Muninn can run shell commands, reach other hosts over SSH, and act on the home unattended. The thing that makes that safe is not the model's good judgement — it is a deterministic gate pipeline that inspects every command before execution. The guiding principle:

Deterministic rules guard the unmistakable; the LLM judges the nuanced

Anything catastrophic or categorically off-limits (partition wipes, fork bombs, reaching a protected host, encoded-payload bypasses) is blocked by hardcoded pattern matching — the model never gets a vote. Only the genuinely ambiguous middle ground (“is git reset --hard HEAD~1 reasonable self-admin?”) is delegated to an LLM safety judge, and that judge is the last gate, not the first. Safety never depends on the prompt being obeyed.

The Gate Pipeline

Every shell invocation passes through these checks in order (src/tools/shell.rs). Any gate can stop the command and return a message to the LLM instead of executing.

0
Self-service control — a command that would restart, stop, or kill Muninn's own service is refused and redirected to the self_restart tool. Absolute; runs before every bypass.
0
Protected hosts — any command mentioning a host in shell_protected_hosts is blocked, even tunnelled through a sandbox hop. These machines are never SSH-reachable.
0a
Obfuscation — interpreter / encoded-payload wrappers (python -c, base64 -d | sh, paramiko/fabric, …) are rejected before the sandbox shortcut, so nothing can smuggle a remote session inside an interpreter.
0b
Remote-exec allowlist — outbound ssh/scp/ rsync/… must target a host in shell_sandbox_hosts or shell_ssh_remote_allowlist. An empty allowlist denies all outbound remote-exec. localhost/127.0.0.1 are always local.
0c
Unsafe file write — a file-content write whose content still carries shell substitution (`…` or $(…)) — an echo/printf redirected/teed to a file, or an unquoted heredoc (<< EOF, not << 'EOF') — is blocked, because bash executes that substitution instead of writing it literally. The rejection points at the safe quoted-heredoc form. Universal: fires even under shell_local_full_access — it is the gate that prevents a rule's literal backticks from running a command and dumping its output into the target file.
1
Fast-reject — a deterministic substring scan against a list of dangerous patterns (recursive deletes, disk/partition destruction, fork bombs, firewall flush, shutdown/reboot, sudo escalation, pipe-to-shell, cluster/container nukes, history wipes, force-push…). Matched before any LLM call.
2
Safety judge (LLM) — whatever survives the deterministic gates is rated safe / caution / dangerous by the model. dangerous blocks; caution is logged and allowed; safe runs. If the judge call fails, the command is blocked (fail-closed).
3
Execute — the command runs with an idle timeout (30 s default; 120 s for sandbox / full-access paths).

The Catastrophic Floor

A hard tripwire that no flag can lift

A small subset of the reject list is irreversible host destruction — recursive wipes of / or $HOME, raw writes to /dev/sd*//dev/nvme*, mkfs/wipefs/ fdisk/parted, and fork bombs. These are caught by fast_reject_catastrophic() and fire even under shell_local_full_access, which otherwise skips the judge and the full reject list. There is no operational reason to run them, so they are blocked deterministically with the model out of the loop entirely.

Full-Access Bypass Flags

Two opt-in flags loosen the gates for trusted self-administration. Crucially, neither lifts Gates 0/0a/0b — a bypassed command still cannot reach a protected host, smuggle an obfuscated payload, or SSH outside the allowlist.

FlagScopeSkipsStill enforced
shell_local_full_access Commands that run on this host (no outbound remote-exec) The safety judge and the full fast-reject list Self-service, protected-hosts, obfuscation, remote-allowlist, and the catastrophic floor
shell_adb_full_access A single bare adb invocation (no top-level shell operators) Fast-reject and the safety judge Self-service, protected-hosts, obfuscation, remote-allowlist

The trade-off is explicit: local_full_access lets Muninn self-administer its own box (sudo, systemctl, apt, docker, git reset, iptables…) but drops the deterministic floor from the full reject list down to the catastrophic subset. Enable it only on a host you trust Muninn to own.

Deterministic vs. LLM

The defining property of the design — almost everything is hardcoded; the model is one gate among many, and the only non-deterministic one.

CheckType
Self-service controlDeterministic
Protected hostsDeterministic
Obfuscation / encoded payloadsDeterministic
Remote-exec allowlistDeterministic
Fast-reject (dangerous patterns)Deterministic
Catastrophic floor (irreversible)Deterministic
Safety judge (safe/caution/dangerous)LLM

Role gating sits in front of all of this

Before a command ever reaches the pipeline, the role-based tool filter decides whether the caller may use shell at all — it is admin-only. The gates above are the second line for the privileged path, not the first. See the Tools and Config pages for the permissions matrix and the shell_* config keys.