Why the spec file alone isn’t enough
agent.enabled: false removes a task from what meriadoc advertises over MCP — it doesn’t stop anything by itself. If the agent calling meriadoc also has its own native shell tool (most coding agents do), it can just run git commit directly and skip meriadoc’s commit task entirely. This is a documented failure mode, not a hypothetical one — “MCP tools don’t respect sandboxing” (openai/codex#7635) describes the same gap from the other direction. The fix is running the agent’s whole process inside a boundary where the disallowed action is physically impossible — which is exactly what modern agent runtimes do under the hood: DeepSeek’s own Harness wraps every subprocess it spawns in Linux Landlock, macOS Seatbelt, or a Windows restricted-token runner, for the same reason.
This example covers Linux via bubblewrap. A macOS version via Seatbelt (sandbox-exec) isn’t included yet — it requires several permissions beyond what Apple’s own documentation covers, and macOS’s handling of relocated or copied binaries under sandboxing adds further complexity. The Docker version works cross-platform in the meantime.
One task generates the sandbox and launches it
No separate .sh wrapper or .sb profile file to save, chmod +x, and keep in sync — each task below writes its own sandbox config to a temp file and immediately execs into it in the same command. AGENT_CLI_PATH is the only thing meriadoc can’t infer for you.
version: v1
tasks:
# ── Policy: what the agent may do once it's inside the sandbox ──
status:
description: "Show working tree status"
agent:
risk_level: low
cmds:
- git status
diff:
description: "Show uncommitted changes"
agent:
risk_level: low
cmds:
- git diff
log:
description: "Show recent commit history"
agent:
risk_level: low
cmds:
- git log --oneline -20
run-tests:
description: "Run the test suite"
agent:
risk_level: low
cmds:
- npm test
commit:
description: "Commit changes (human only — not exposed to the review agent)"
agent:
enabled: false
cmds:
- git commit
# ── Sandbox: meriadoc generates the profile and launches the agent inside it ──
sandbox-start-linux:
description: "Launch the agent inside a bubblewrap sandbox (Linux) — network-less, read-only repo mount"
env:
AGENT_CLI_PATH:
type: filepath
required: true
preconditions:
- cmds:
- command -v bwrap
on_failure:
continue: false
cmds:
- echo "bubblewrap not found — install with 'apt install bubblewrap' (Debian/Ubuntu) or 'dnf install bubblewrap' (Fedora)"
cmds:
- |
set -e
CONFIG_FILE=$(mktemp)
cat > "$$CONFIG_FILE" <<'CONFIG_EOF'
discovery:
roots:
- path: /workspace
enabled: true
CONFIG_EOF
AGENT_DIR=$(dirname "$AGENT_CLI_PATH")
bwrap \
--unshare-net \
--die-with-parent \
--ro-bind /usr /usr \
--ro-bind /bin /bin \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--ro-bind "$$AGENT_DIR" "$$AGENT_DIR" \
--ro-bind "$(pwd)" /workspace \
--ro-bind "$$CONFIG_FILE" /etc/meriadoc-config.yaml \
--bind /tmp /tmp \
--chdir /workspace \
--clearenv \
--setenv HOME /tmp \
--setenv PATH "/usr/local/bin:/usr/bin:/bin:$$AGENT_DIR" \
--setenv MERIADOC_CONFIG /etc/meriadoc-config.yaml \
-- "$AGENT_CLI_PATH" --mcp-server "meriadoc serve"
rm -f "$$CONFIG_FILE"
Run it
meriadoc task sandbox-start-linux --env AGENT_CLI_PATH=/usr/local/bin/claude
The task generates a throwaway discovery config (so the sandboxed meriadoc serve can find /workspace’s tasks), runs the agent inside the bubblewrap sandbox, then cleans up. --unshare-net drops networking entirely; the repo, the agent binary, and the toolchain are all read-only; only /tmp is writable. commit is unreachable both because meriadoc won’t offer it, and because the sandboxed process has no writable path back to .git and no network to do anything with the result anyway.