Scenario
You want rousseau to inspect a repository, answer questions about it, and produce reports — but it must not be able to write, edit, or run destructive shell commands. This is the posture you would deploy for a first-pass audit, an incident-response inspection, or a compliance walk-through.
Three layers stack to make this hard:
- Approval policy — deny every mutating tool.
claudeclipermission mode — put Claude Code inplanmode so its own approver never edits files.- Filesystem — bind-mount the workspace read-only.
Belt, braces, and a second belt. Any one of the three fails safely.
Layer 1 — Approver
The simplest read-only posture uses the pattern approver with a whitelist:
agent:
approver:
mode: pattern
default: deny
reason: "read-only inspection posture — this deployment cannot mutate files"
allow:
- {tool: read, match: ".*"}
- {tool: grep, match: ".*"}
- {tool: bash, match: "^\\s*\"command\":\\s*\"(ls|cat|head|tail|find|wc|stat|file|which|pwd|env|git status|git diff|git log|git show|git branch)\\b"}
# No deny rules needed — default: deny catches everything else.
# No edit, write, or unrestricted bash — the model can't reach them.
An even stricter variant uses deny_all, which blocks every tool including read and grep:
agent:
approver:
mode: deny_all
reason: "smoke test — no tool calls allowed"
deny_all is only useful as a smoke test; the model won't be able to do meaningful work.
Layer 2 — claudecli permission mode
When the provider is claudecli, Claude Code itself is running the tool calls. Setting permission_mode: plan makes Claude Code refuse every write- or edit-side call in its own layer, even if the rousseau approver would have allowed it:
provider: claudecli
claudecli:
binary: claude
permission_mode: plan
Valid values (see internal/config/config.go and Claude Code's docs): acceptEdits, auto, bypassPermissions, default, dontAsk, plan. plan is the only value that consistently keeps Claude Code in read-only posture.
Layer 3 — Filesystem
Mount the workspace read-only. Under the reference Podman Quadlet:
Volume=%h/team-rousseau-workspace:/workspace:ro,Z
ro makes the mount read-only from the container's perspective; even if a compromised binary tried to open(2) with O_WRONLY, the kernel would return EROFS.
Under Kubernetes:
volumeMounts:
- name: workspace
mountPath: /workspace
readOnly: true
The session store (~/.local/share/rousseau/) still needs to be writable — the daemon appends to it on every turn. Keep that mount rw and leave only the workspace read-only.
Dry-run posture
There is no --dry-run flag on the daemon. If you want the model to plan changes without executing them, the combination above achieves the equivalent:
- The approver blocks every mutating tool → the model gets a
tool_resulterror explaining the block. planmode inclaudeclikeeps Claude Code from running its own destructive tools.- Read-only mounts stop anything that leaks through.
The model will typically respond with a plan document rather than a diff. That is the read-only inspection deliverable.
What still works
- Every
readandgrepcall. bashfor safe read-side utilities you enumerated.- Session persistence — the SQLite store still records the conversation.
- Cross-session recall via FTS5, MCP export, skills — all read-only anyway.
What breaks (intentionally)
writeandedit— deny.- Shell mutation commands — deny.
- Cron jobs whose prompt implies file writes — the model tries, gets denied, replies with a plan.
rousseau init— the CLI is not affected by the approver, but it writes to~/.config/rousseau/outside the workspace. Run it before rolling out read-only mode.
Testing the posture
rousseau chat
> Edit /workspace/README.md to add a footer.
Expected log line:
WARN tool.denied name=edit reason="read-only inspection posture — this deployment cannot mutate files"
Expected chat reply: the model apologises, produces a plan or a diff patch as text, and asks the operator to apply it.
For the deny_all variant, every tool call is blocked — the model has no way to inspect anything, so this posture is only useful as a smoke test.
Layering with other transports
The same three layers apply to WhatsApp, Slack, Discord, and every other transport. Because the approver runs inside the agent loop, it does not care which transport delivered the user turn. A read-only Slack agent is one mode: pattern block away.
Caveats
- The read-only posture is enforced by rousseau's approver and by the filesystem — not by the LLM. A model can still emit an
edittool call; the approver silently blocks it, but the attempt is logged astool.denied. This is intentional so audit trails record what the model tried, not just what succeeded. - Read-only bind mounts do not protect against symlinks pointing outside the mount. The reference Podman posture drops all capabilities, which prevents most escape paths, but do not rely on the mount alone.
- The
claudecliprovider'splanmode is Claude Code's contract, not rousseau's. If Claude Code changes its permission-mode semantics, rousseau's read-only posture inherits that change.
Next
- User Guide: Approval Policies — deeper reference.
- Audit + approval policies — the mutating counterpart.
- Deployment — mount and container flags.