?

The two tools

Two tools mutate the filesystem:

  • write — full-file overwrite. internal/tools/builtin/write.go writes with mode 0o644 and MkdirAll(dir, 0o755).
  • edit — single exact-string replacement inside an existing file. internal/tools/builtin/edit.go.

Both require an absolute path (they call filepath.IsAbs). Both perform no atomic-swap dance — they use os.WriteFile directly.

The container view of the world

The reference Quadlet unit at docker/rousseau-agent.container mounts three host directories into the container:

Volume=%h/.local/share/rousseau:/home/rousseau/.local/share/rousseau:rw,Z
Volume=%h/.claude:/home/rousseau/.claude:rw,Z
Volume=%h/team-rousseau-workspace:/workspace:rw,Z

Nothing else on the host is visible. From inside the container, an edit tool call against /workspace/repos/foo/main.go resolves to ~/team-rousseau-workspace/repos/foo/main.go on the host.

:Z — the SELinux label

The :Z flag on each Volume= tells Podman to relabel the mount with a container-private SELinux MCS category. Without it, on a system with SELinux in enforcing mode:

  • Reads still work most of the time (container_file_t is broadly readable).
  • Writes fail with EACCES and avc: denied { write } in the audit log.

If you swap the flag for :z (lowercase), Podman relabels with a shared category — safer for hosts you share between multiple container users, but not the default.

On systems without SELinux (Debian, non-hardened Ubuntu), :Z is a silent no-op.

UserNS=keep-id — UID mapping

The container runs as UID/GID 1000. Without user-namespace mapping, rootless Podman would remap 1000 into the subuid range (typically 100000+), and files written from inside the container would be owned by that mapped UID on the host — unusable for the operator.

UserNS=keep-id maps container UID 1000 to the host user's UID (also 1000 in the reference setup). Files written inside /workspace end up owned by seb:seb on the host — exactly what you want.

If your host user is not UID 1000, the mapping still works; keep-id uses the invoking user's actual UID.

Editing outside /workspace

Because the bind mounts are the container's only view of the host filesystem, write or edit against /etc/nginx/nginx.conf will fail with a path-not-found error — the path simply doesn't exist inside the container. This is a feature: it means the operator's approver policy can trust the container boundary.

If you genuinely need the daemon to touch a different host path:

  1. Preferred: add a new Volume= line to the Quadlet unit. Make the least-permissive choice: :ro for read-only, :Z for private SELinux labelling.
  2. Do not run rousseau outside the container to bypass the boundary — you lose seccomp, drop-caps, and the read-only root filesystem.

Editing outside the container

If you run rousseau directly on the host (no container), the tools operate against the daemon's process view — everything under the user's HOME by default. The approver is the only containment layer. See Guides: Audit + approval policies for the pattern-mode + default: deny recipe.

write vs edit — when to use which

Situation Use
Creating a new file. write.
Rewriting a file wholesale. write.
Changing one section of a large file. edit. It fails safely when old_string is not unique.
Renaming a symbol across the file. Multiple edit calls with progressively more surrounding context, or a single write with the full rewritten contents. Do not use edit with replace_all-style semantics — the tool refuses.

The exact-uniqueness constraint on edit is deliberate. It borrows directly from Claude Code's Edit tool. Search internal/tools/builtin/edit.go for the comment block that explains why.

Common failure modes

Symptom Cause Fix
edit: path must be absolute, got "…" Model passed a relative path. Reject or rewrite in the approver; ask the model to use absolute paths.
edit: old_string not found in … The file changed since the model last read it, or the model hallucinated surrounding context. The model will typically read again and retry.
edit: old_string is not unique in … (found 3 occurrences) Same string appears multiple times. Model must supply more surrounding lines to disambiguate.
write: permission denied SELinux label mismatch or wrong UID mapping. Verify :Z on the volume and UserNS=keep-id on the container.
read: does not look like UTF-8 text File contains NUL bytes in first 512 bytes (isLikelyText in read.go). Refuse binary reads at the approver level; use the bash tool with file if identification is needed.

Backups before big rewrites

The tools do not create .bak copies. For high-risk changes, teach the model to write to a sibling path first, bash diff it, then swap. Alternatively, run everything through a git branch — rousseau leaves git completely out of its execution path, so any versioning happens through your normal workflow.

Related

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