?

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:

  1. Approval policy — deny every mutating tool.
  2. claudecli permission mode — put Claude Code in plan mode so its own approver never edits files.
  3. 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_result error explaining the block.
  • plan mode in claudecli keeps 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 read and grep call.
  • bash for 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)

  • write and edit — 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 edit tool call; the approver silently blocks it, but the attempt is logged as tool.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 claudecli provider's plan mode 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

Type to search 150+ pages. Ranking: BM25 with title/description boost.