1 Container
Thomas Forgione edited this page 2026-09-11 13:32:34 +02:00

In a container

papote has a tool that runs shell commands, and the sensible place to point that at is a machine you would not mind losing. The Dockerfile in the repository builds one.

./run.sh              # builds, then runs
./run.sh --private    # arguments are passed through

which is this, with the directories under volumes/ bind-mounted:

docker build -t papote .
docker run -it --rm \
  -e TERM="$TERM" \
  -v ./volumes/config:/home/papote/.config/papote \
  -v ./volumes/app:/home/papote/app \
  -v ./volumes/conversations:/home/papote/.local/share/papote/conversations \
  papote "$@"

-it is not optional: papote takes the whole terminal, and without a tty there is nothing to take. TERM is passed through so the colours match what your terminal can actually do; the image defaults to xterm-256color.

The container starts in ~/app, which is where a relative path from a tool lands and where run_shell runs. Nothing is declared a volume in the Dockerfile — what to mount and where is yours to decide.

What is in it

Debian trixie, the papote binary, and ca-certificates, which is the whole of what papote itself needs: rustls wants the trust store, and nothing else is linked. The clipboard finds nothing in here, so Ctrl-V has nothing to paste — everything else works.

The rest is for the model: git, curl, jq, less and ripgrep. A model given a machine and nothing to run on it asks for git on its first turn. It can install more itself — papote may sudo without a password, because the container is the wall and working around a missing package is not what you want it spending its turns on.

Who it runs as

Not root: a papote user, uid 1000 by default. Build with --build-arg UID=$(id -u) if yours is not.

Under rootless Docker this has a consequence worth knowing before you hit it. Rootless maps the container's users into your subuid range:

in the container on the host
root you
papote (1000) 101000, a subuid

So everything the model writes into a bind mount belongs to that subuid, and you need sudo to edit or delete it from the host — your own providers.toml included, once papote has written the directory. Running the container as root instead (--user 0:0) would make those files yours, and under rootless the container's root is your unprivileged self anyway.

Both are defensible. The Dockerfile keeps the papote user because on a rootful daemon — a server — that difference is a real one.

What the container does not protect

Your endpoint credentials. providers.toml holds the password in clear and is mounted in, so a model that asks the shell to read it gets to read it. You would see the call go past in the confirmation, but the container is a wall around your machine, not around what papote itself can see. If that matters, give the container its own account on the endpoint.