Skip to content

Server CLI & environment

Flags and environment variables for the default server invocation — painapple [flags] (or explicitly painapple serve [flags], or python -m painapple_code [flags] from a repo checkout).

painapple help (or a leading -h/--help) prints a curated command overview — serve, setup, list, start/stop/restart, status/logs/password, container verbs — with the most common flags; painapple serve --help prints the full flag reference below. A bare painapple always serves the directory you launch it from (--workspace overrides). painapple list (or a bare painapple status) shows every deployment on the machine — the root one, every saved profile host or docker, and any unmanaged server processes. painapple start/stop/restart manage instances in the background.

Saved defaults — painapple setup

painapple setup is an interactive wizard that saves global defaults to ~/.painapple-code/serve.yaml: the network bind (host/port/TLS) a bare painapple starts with, and the container runtime + image used by --in-docker. Explicit flags always override the saved values. Two quick sections plus a review screen; nothing is written until you confirm.

Workspace and cosmetics are profile-only since the CLI unification — a bare painapple serves the cwd, and a label/accent belongs to a named deployment. painapple setup NAME creates/edits that profile instead (host or docker mode).

# ~/.painapple-code/serve.yaml — written by `painapple setup`, editable by hand
host: 127.0.0.1
port: 8765
tls: auto
runtime: docker         # --in-docker: docker | podman | /path/to/binary
image: painapple-code:latest

Recognized keys: host, port, tls (serve defaults — each maps 1:1 onto the flag of the same name below) plus runtime, runtime_flags, image (read only by the --in-docker launch path). Unknown keys, invalid values, and leftover pre-unification keys (workspace, instance_name, accent) are skipped with a warning in the server log. The file lives under PAINAPPLE_CODE_HOME (default ~/.painapple-code). Precedence: built-in default < serve.yaml < explicit flag — so systemd units and the Docker entrypoint, which pass everything explicitly, are unaffected.

Container mode is a flag, not a command group

painapple --in-docker runs the same invocation in a Docker/Podman container, and profiles carry a mode: host | docker — see Profiles & container mode. The old painapple docker command group is gone.

Flags

Defaults shown are the built-ins — values saved by painapple setup replace them; explicit flags override both.

Server — where it listens and what it works on

Flag Default Description
--host 127.0.0.1 Host interface to bind — 127.0.0.1 = this machine only, 0.0.0.0 = every interface (reachable on your LAN)
--port 8765 Port to bind to
--workspace . Workspace directory — the directory Claude operates in. Mapped to /workspace inside Docker. (--cwd is an alias.)
--workspace-root --workspace Directory scanned for sibling project folders surfaced on the welcome screen (the "Unvisited" chips)
--instance-name Instance label for the PWA icon and UI (e.g. DEV, STABLE)
--accent Accent color: a preset name (see below) or a hex value like #f87171
--profile Run a named profile in the foreground (host: its own isolated data home; docker: its container)
--in-docker off Run this same invocation inside a container instead — see Profiles & container mode

Network security

Flag Default Description
--tls auto TLS mode: auto, on, or off (see below)
--tls-cert <config-dir>/cert.pem TLS certificate path (auto-generated if missing)
--tls-key <config-dir>/key.pem TLS key path (auto-generated if missing)

Advanced

Flag Default Description
--default-provider claude-sdk Default AI engine for new sessions (existing sessions keep their recorded engine). Pinning it here also hides the UI's "Make default" button
--enable-eruda off Enable the Eruda mobile-devtools quick action (loads from a CDN)
-v, --version Print version and exit

Multi-instance — isolate state when several servers share one user

Flag Default Description
--shadow-db ~/.painapple-code/shadow.duckdb DuckDB path for the shadow turn store
--log-dir ~/.painapple-code/logs/ Log directory
--state-suffix Per-tier suffix for UI-state files (tab-state, shortcuts, presets, favorites, global config) so co-located instances don't share them. --state-suffix dev gives tab-state-dev.json; a leading - is added automatically. Project and session history stays shared.
--auth-config-file ~/.config/painapple-code/config.yaml Auth config file path

TLS behavior

  • --tls auto (the default) enables TLS only when binding to a non-loopback host. On 127.0.0.1, ::1, or localhost the server stays plain HTTP.
  • --tls on forces TLS; --tls off disables it even on non-loopback binds (the server logs a loud warning — your auth token and chat contents travel the LAN unencrypted).
  • When TLS is enabled, a self-signed certificate is auto-generated at <config-dir>/cert.pem / key.pem (next to the auth config file). There is no OS trust-store install; browsers show a one-time certificate warning.

Non-loopback binds

Binding to a non-loopback host also makes the server trust X-Forwarded-Proto from any client (forwarded_allow_ips is *). For real exposure, put the bridge behind a reverse proxy that sets that header itself — see Read this first.

Filesystem access

The bridge browses, reads and writes anywhere its OS user can — your home directory, /data, /srv, a NAS mount, Docker's /workspace, wherever. There is no path allowlist. The only exclusions are /proc, /sys and /dev, which are skipped for practical reasons (kernel-special files like the 128 TB /proc/kcore make the file browser hang, and editing them is meaningless).

That's deliberate, not an oversight: everyone reaching these endpoints is already past the password gate, and an authenticated session comes with a full PTY terminal, !bang shell commands, and an agent running as the same user. A path allowlist on the editor would stop nothing that isn't one shell line away, while breaking legitimate edits to projects outside $HOME. Unix file permissions are the real boundary — run the bridge as a user that can only touch what you're willing to expose, and don't run it as root.

Environment variables

Variable Purpose
PAINAPPLE_CODE_HOME Override the data directory (default ~/.painapple-code). Set to /data in the Docker image.
PAINAPPLE_PROFILE Default profile when --profile isn't given.
PAINAPPLE_CODE_CONFIG Override the config directory (default ~/.config/painapple-code) — where config.yaml and the auto-generated TLS cert/key live.
BRIDGE_ALLOWED_ORIGINS Comma-separated CORS allow-list. Defaults to localhost / 127.0.0.1 on ports 8765, 8800, and 8880. Set this when serving from a public hostname.
PAINAPPLE_REVEAL_CMD Exact "reveal password" command shown verbatim on the login page. Launchers (the Docker wrapper) set this because they know the host-side container name and engine; unset, the page falls back to a per-environment guess.
PAINAPPLE_IN_CONTAINER Set to 1 inside the official image. Gates the filesystem probes that distinguish Docker from Podman for the login page's environment detection — never set this on a bare-metal host.

Accent color presets

--accent accepts any of these preset names, or an arbitrary hex color (--accent '#e11d48'):

Preset Hex
blue #58a6ff
green #22c55e
red #f87171
orange #fb923c
purple #c084fc
cyan #06b6d4
gray (or grey) #9ca3af
yellow #facc15
pink #f472b6
teal #14b8a6
indigo #818cf8
lime #a3e635

Profiles — multiple deployments

painapple --profile NAME (or PAINAPPLE_PROFILE=NAME in the environment) runs a named, fully independent deployment in the foreground. A host-mode profile gets its own data home at ~/.painapple-code/profiles/NAME/: sessions, shadow DB, logs, global config, and its profile.yaml, all isolated from the default instance (this isn't cosmetic — the DuckDB turn store is single-writer, so two servers can never share one data home). A docker-mode profile runs as its container instead. Full reference: Profiles & container mode.

painapple setup work               # create/edit the profile (mode, workspace, port, …)
painapple start work               # run it in the background — alongside the default instance
painapple --profile work           # …or run it in the foreground
painapple list                     # everything shows up, host and docker alike

Profile names are letters, digits, ., _, - (max 32); default is reserved for the flag-less root deployment (the classic ~/.painapple-code home). If a profile doesn't set an instance_name, the profile name is used as the UI label, so co-running instances stay distinguishable.

Background lifecycle — painapple start/stop/restart

There is no daemon — a painapple server process is the instance — so these commands find their target the same way painapple list does (process scan; exact data-home match first, port match only when the home can't be read) and manage it with plain signals. The name resolves in order: saved profile → instance label → PID → port — anything painapple list prints is a valid target:

painapple start work               # spawn detached, wait for the port, print the login URL
painapple stop work                # SIGTERM, escalate to SIGKILL after 10s
painapple restart work             # stop (if running) + start
painapple restart SENUTO           # by instance label (case-insensitive) …
painapple restart 16187            # …or by pid …
painapple stop 8766                # …or by port
painapple start                    # no name = the flag-less default deployment
painapple start work --port 9001   # extra serve flags apply to THIS start only (not saved)

start targets a saved profile (painapple setup NAME first) — an unknown name is refused rather than silently spawning an empty deployment, and a port already occupied by a different instance is refused up front. A docker-mode profile delegates to the container runtime instead: start = docker run -d --restart unless-stopped (durable across reboots), stop = docker stop. A label/pid/port target has no saved config (it was launched ad hoc with flags), so restart recaptures the live process's own command line, working directory, and environment (/proc on Linux; a lossier ps-based fallback elsewhere) and respawns it verbatim. Console output goes to <data-home>/logs/console.log; the regular server.log lives next to it. A failed startup prints the tail of the console log. For an always-on host instance prefer a service manager (systemd, launchd) that restarts on exit — host start spawns once and does not supervise.

painapple status NAME, painapple logs NAME, and painapple password [NAME] inspect any deployment — host or docker — without touching it. status also takes an unmanaged process's label, PID, or port, and a bare painapple status is the fleet view (painapple status default is the root deployment's own detail block).

With no NAME logs/password target the root deployment, which is the host one. The ad-hoc painapple --in-docker sandbox has no name to select it by, so it takes the mode flag instead: painapple password --in-docker, painapple logs --in-docker. (Its own login page prints the right command for you.) If no host bridge has ever run and the sandbox is up, a bare painapple password answers for the sandbox rather than reporting nothing.

Running multiple instances (manual flags)

Prefer profiles for full isolation. Alternatively, --instance-name, --accent, --state-suffix, --shadow-db, and --log-dir together run several tiers side by side under one user sharing project/session history, without stepping on each other's state:

painapple --port 8880 --instance-name DEV --accent red \
  --state-suffix dev \
  --shadow-db ~/.painapple-code/shadow-dev.duckdb \
  --log-dir ~/.painapple-code/logs-dev

Each instance needs its own shadow DB — DuckDB is single-writer.

The fleet view — painapple list

painapple list (aliases ls, ps, instances, profiles) and a bare painapple status render the same overview, in two sections:

  • Deployments — everything a NAME verb targets: the root default deployment first, then every saved profile, each with a [host]/[docker] badge, its address, and its running state. A running server is matched to its deployment by data home (exact) and port, so the process behind default is shown as default — with its --instance-name label alongside the PID when it has one.
  • Unmanaged processes — painapple servers in the process table that belong to no deployment: launched by hand or by a service unit with their own flags. Their name is just their --instance-name label, and stop/restart/status/logs accept it (or their PID or port).

The ad-hoc painapple --in-docker container, when one exists, gets its own line below.

Known limitations

This is an MVP; some corners are honestly rough:

  1. Windowing system — works, but doesn't support multiple instances of the same widget and could use a rethink.
  2. Code editor — currently a notepad with syntax highlighting. The plan is a review-driven workflow rather than a VSCode-grade editor; the markdown inline editor is the exception and works well for plan and doc tweaks.
  3. GUI for OS-level features — the git widget and file explorer exist, but the embedded terminal is often the better tool for grep / sed / find / du, so these widgets haven't been a priority.