Revise config redesign against the claude-code branch
Treat the claude-code branch as the baseline for agent support instead
of proposing a parallel design. Refocus the doc on the remaining gaps:
include-based config sharing, project-local and CLI override layers, and
outbound sharing via jj-backed writable mounts with the outbox as a
review-before fallback. Flag branch loose ends found while reconciling:
the pending rebase conflicts with main, the shared ramekin-agent tag
across agents, and README drift around prompt injection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LDUQnA7QT8CtTfqZ6JE5vJ
diff --git a/docs/config-redesign.md b/docs/config-redesign.md
index e9aef12..a4467d5 100644
--- a/docs/config-redesign.md
+++ b/docs/config-redesign.md
@@ -1,79 +1,64 @@
# Config redesign
-Status: proposal (2026-07)
+Status: proposal (2026-07), revised against the `claude-code` branch
## Goals
-1. **Multiple agents** — run either pi or Claude Code in the container, selected
- per run or per project, without forking the codebase.
-2. **Shareable config** — the interesting parts of my setup (AGENTS.md, skills,
- mounts, agent choice) should live in one place (dotfiles) and be usable from
- any machine and any project.
-3. **Overridable everything** — any shared setting can be overridden closer to
- the point of use: per machine, per project, per run.
-4. **Safe outbound sharing** — when the agent improves a skill or AGENTS.md
- inside the container, there's a reviewed path for that change to land back
- in the shared source of truth. Never direct writes to host dotfiles.
-
-## What's wrong today
-
-- pi is hardcoded everywhere: the `pi` config section, `/root/.pi` mounts, the
- `pi` entrypoint, `--append-system-prompt` with a file path, the npm package
- in the Dockerfile.
-- Only two config files exist (user + project) and there is no way to compose
- them from a shared source. "Sharing" today means hand-symlinking
- `~/.config/ramekin/config.kdl` into dotfiles, with no room for
- machine-specific deviation.
-- The agent dir is assembled by *copying* sources in and clearing everything
- (except `auth.json`) on the next run. Config flows one way; in-container
- edits to skills or AGENTS.md silently evaporate.
-- No run-level overrides: can't say `--agent claude` or add a one-off mount
- without editing a file.
-
-## Design
-
-### 1. Agents are builtin profiles, config selects one
-
-Since I'm the only user, agent definitions live in Rust, not in config. Each
-agent knows its own plumbing:
-
-| | pi | claude |
-|---|---|---|
-| package | `@mariozechner/pi-coding-agent` | `@anthropic-ai/claude-code` |
-| entrypoint | `pi` | `claude` |
-| state dir (persisted) | `/root/.pi` | `/root/.claude` + `/root/.claude.json` |
-| assembled config dir | `/root/.pi/agent` | `/root/.claude` |
-| auth files (preserved on clear) | `auth.json` | `.credentials.json` |
-| memory file | `AGENTS.md` | `CLAUDE.md` |
-| per-repo sessions | mount at `/root/.pi/agent/sessions` | mount at `/root/.claude/projects` |
-| container prompt | `--append-system-prompt <file>` | appended to assembled `CLAUDE.md` |
-
-Notes on the claude column:
-
-- Claude Code's `--append-system-prompt` takes text, not a path, and the
- compose command is baked at generation time. Rather than shell-quoting a
- `$(cat ...)`, ramekin owns assembly anyway — so it appends the
- ramekin-prompt content as a section of the assembled memory file. The same
- mechanism works for pi if we ever want to drop the flag.
-- Claude keys session history by project path under `~/.claude/projects/`.
- Every ramekin workspace is `/workspace`, so all projects would collide on
- one key. Mounting the per-repo sessions dir at `/root/.claude/projects`
- keeps histories separate, same trick as pi's sessions mount.
-- `~/.claude.json` (onboarding state, MCP servers, project trust) lives
- *next to* the state dir, not in it. The claude profile persists it as a
- file mount from the ramekin data dir.
-
-Selection, lowest to highest precedence: builtin default (`pi`), then
-`agent "claude"` in any config layer, then `ramekin run --agent claude`.
-The Dockerfile installs both agents; they're small and it keeps one image.
-
-XDG state splits per agent: `$XDG_DATA_HOME/ramekin/pi/`,
-`$XDG_DATA_HOME/ramekin/claude/`, and per-repo sessions under
-`$XDG_DATA_HOME/ramekin/repos/<slug>/<agent>-sessions/`. Existing pi state
-migrates with a one-time move (or just keep `$XDG_DATA_HOME/ramekin/` as the
-pi dir and add `claude/` beside it — cheaper, slightly asymmetric).
-
-### 2. Sharing in: `include` + a config search path
+1. **Multiple agents** — run either pi or Claude Code in the container,
+ selected per project or per run.
+2. **Shareable config** — the interesting parts of my setup (memory files,
+ skills, mounts, agent choice) live in one place (dotfiles) and are usable
+ from any machine and any project.
+3. **Overridable everything** — any shared setting can be overridden closer
+ to the point of use: per machine, per project, per run.
+4. **Safe outbound sharing** — when the agent improves a skill or memory file
+ inside the container, there's a safe path for that change to land back in
+ the shared source of truth.
+
+## Baseline: what `claude-code` already delivers
+
+The `claude-code` branch covers goal 1 end to end, and its choices supersede
+an earlier draft of this doc:
+
+- `agent "pi" | "claude"` as a top-level KDL scalar; highest-precedence layer
+ wins, defaulting to pi. `AgentLayout` in `main.rs` carries each agent's
+ paths, builtin mounts, embedded Dockerfile, and prompt plumbing.
+- **Bind mounts, not copies, for claude.** `claude { ... }` entries expand to
+ read-only-by-default bind mounts at `/root/.claude/<target>`. Host edits
+ are live in the container; auth and runtime state in the surrounding
+ `~/.claude` mount are untouched; no clear-and-reassemble ceremony. The
+ `pi { ... }` copy-assembly model stays as-is — the asymmetry is deliberate,
+ matching how each agent treats its config dir.
+- **Per-slug workspace instead of a sessions mount.** Claude keys its
+ `projects` map and transcripts by cwd, so mounting each workspace at
+ `/workspace/<slug>` isolates repos while `~/.claude` and `~/.claude.json`
+ stay global (auth, identity, onboarding survive repo switches).
+- Yolo mode via managed settings baked into the image
+ (`bypassPermissions` + skip dialog + `IS_SANDBOX=1`), not CLI flags.
+- Separate `Dockerfile.claude`, GitHub token forwarded as a BuildKit secret,
+ side-effect-free `ramekin config`, deterministic parent-before-child mount
+ ordering.
+
+### Loose ends on the branch itself
+
+- **Rebase onto main.** `claude-code` forked before the last five main
+ commits (project image tags, `--rebuild` semantics, file-source mounts,
+ justfile CI). README.md and src/main.rs conflict; the project-image-tag
+ work on main overlaps with the shared `ramekin-agent` tag assumption below.
+- **Shared base tag across agents.** Both Dockerfiles build to
+ `ramekin-agent`, and project Dockerfiles say `FROM ramekin-agent`.
+ Switching agents silently swaps what that tag means and busts every
+ project layer built on the other agent. Per-agent tags
+ (`ramekin-agent-pi`, `ramekin-agent-claude`) with the project `FROM`
+ rewritten or parametrized via build arg would fix the churn.
+- **README drift.** The branch README says claude gets the prompt via
+ `--append-system-prompt`, but the code passes `--append-system-prompt-file`;
+ the pi section describes a `ramekin.ts` extension while the code writes
+ `ramekin-prompt.md` and passes a flag. Reconcile during the rebase.
+
+## Remaining work
+
+### 1. Sharing in: `include`
Any config file may include others:
@@ -81,7 +66,7 @@ Any config file may include others:
// ~/.config/ramekin/config.kdl — machine-specific, tiny
include "~/.dotfiles/ramekin/config.kdl" // the shared base
-// machine-only override:
+// machine-only additions/overrides below
mounts {
source "~/.local/share/ranger"
writable
@@ -90,127 +75,86 @@ mounts {
Rules:
-- Included files load as their own layer at *lower* precedence than the file
- that includes them (the includer overrides what it includes).
-- Includes may nest; cycles are an error. A missing include is an error too —
- unlike mount sources, a dangling include means the config is wrong, not
- that a host happens to lack a directory.
+- Included files load as their own layer at *lower* precedence than the
+ includer — the machine file overrides the shared base.
+- Includes may nest; cycles are an error. A missing include is an error too:
+ unlike mount sources (where absence is a host fact), a dangling include
+ means the config is wrong.
- `include` accepts a file or a directory (loads `*.kdl` sorted by name).
-This makes the shared config a plain directory in dotfiles, versioned with
-jj like everything else. New machine setup is one line of user config.
+The shared config becomes a plain directory in dotfiles, versioned with jj.
+New machine setup is one line of user config. This composes with the
+`claude {}` / `pi {}` blocks already on the branch — the shared file declares
+the dotfiles-sourced entries once, for both agents.
-### 3. Overriding: more layers, same merge semantics
+### 2. Overriding: two more layers
Layer order, lowest to highest precedence:
-1. **builtin defaults** (agent = pi, builtin mounts — still not overridable)
+1. **builtin defaults** (agent = pi; builtin mounts stay non-overridable)
2. **included files**, in include order
3. **user** `~/.config/ramekin/config.kdl`
4. **project** `<workspace>/.ramekin/config.kdl`
-5. **project-local** `<workspace>/.ramekin/config.local.kdl` (gitignored;
- for things true of this checkout on this machine only)
-6. **CLI** `--agent`, `--mount src[:target][:rw]`, `--env K=V`
-
-Merge semantics stay what they are: mounts and files dedupe by resolved
-target, env by name, scalars (like `agent`) last-writer-wins. The existing
-`/dev/null` masking trick remains the way to *remove* an inherited mount.
-`ramekin config` already shows which scope won; it grows the new layers.
-
-### 4. Config schema: `pi {}` becomes agent-agnostic `files {}`
-
-```kdl
-agent "claude"
-
-mounts {
- source "~/.config/jj"
-}
-
-env FOO="bar"
-
-// Copied into the selected agent's config dir at assembly time.
-files {
- - { source "~/.dotfiles/ai/_AGENTS.md"; target "@memory" }
- - { source "~/.dotfiles/ai/skills" }
-}
-
-// Agent-scoped variants win over unscoped ones for that agent
-// and are ignored entirely for other agents.
-files agent="claude" {
- - { source "~/.dotfiles/ai/claude-settings.json"; target "settings.json" }
-}
-```
-
-- `target "@memory"` resolves to the agent's memory file name (`AGENTS.md`
- for pi, `CLAUDE.md` for claude), so one dotfiles source serves both.
- Other targets are plain paths relative to the agent config dir, as today.
-- The `pi {}` section keeps parsing for one release with a deprecation warning
- (it maps to `files {}`), then dies. Only my configs need updating.
-
-### 5. Sharing out: the outbox
-
-The agent dir stays copy-in/clear-out — that's what makes runs reproducible.
-The new outbound channel is a dedicated writable mount plus a review gate:
-
-- Every session mounts a fresh, empty host dir (under
- `$XDG_DATA_HOME/ramekin/repos/<slug>/outbox/<session-id>/`) at
- `/root/.ramekin/outbox`, writable.
-- `ramekin-prompt.md` grows a section telling the agent: to propose a change
- to shared configuration (skills, AGENTS.md, settings), write the changed
- file into the outbox *mirroring the agent-config-dir layout*, and mention
- it to the user. Everything else about the agent dir is throwaway.
-- On the host, `ramekin outbox` manages proposals:
- - `ramekin outbox` / `outbox list` — pending proposals across sessions
- - `ramekin outbox diff` — difftastic against the *source* file the `files`
- entry was assembled from (ramekin knows the mapping target → source)
- - `ramekin outbox apply` — copy over the source after confirmation, then
- delete the proposal
- - `ramekin outbox discard`
-- After `apply`, the change lands in the dotfiles working copy, where jj
- makes it a normal reviewable/revertable commit.
-
-Why this shape and not the alternatives:
-
-- **Mount dotfiles writable into the container**: simplest, but the whole
- point of ramekin is that the agent can't touch the host outside
- `/workspace`. Rejected.
-- **Bare-repo push channel** (container commits to a mounted bare repo, host
- pulls): safest and most jj-native, but a lot of machinery — seeding a repo
- per session, teaching the agent to commit to it, host-side merge UX. The
- outbox gets 90% of the value with a `read_dir` and a copy. The outbox
- layout is also forward-compatible: if it ever feels limiting, the same
- mount point can become a working copy backed by a bare repo without
- changing the agent-facing contract.
-
-Safety properties: the container only ever writes to a session-scoped empty
-dir; nothing reaches dotfiles without an explicit `apply`; diffs are against
-known sources so a malicious or confused proposal is visible before it lands;
-proposals for targets that don't map back to a `files` source are shown but
-require an explicit destination to apply.
-
-## Migration sketch
-
-Roughly one commit each, in order:
-
-1. Introduce the builtin agent profile struct; thread it through
- `Ramekin::resolve` replacing hardcoded pi paths/flags. pi remains the only
- profile — pure refactor.
-2. Add the claude profile: Dockerfile installs both agents, compose entrypoint
- comes from the profile, memory-file prompt injection, `--agent` flag +
- `agent` config key.
-3. Rename `pi {}` → `files {}` with `@memory` target and agent-scoped
- sections; deprecation shim for `pi {}`.
-4. Add `include`, project-local layer, and CLI `--mount`/`--env` overrides.
-5. Outbox mount + prompt section + `ramekin outbox` subcommand.
-
-Each step lands independently; 1–2 deliver the claude support, 3–4 the
-sharing/override story, 5 the outbound channel.
+5. **project-local** `<workspace>/.ramekin/config.local.kdl` — gitignored,
+ for things true of this checkout on this machine only
+6. **CLI** — at minimum `--agent`; `--mount` and `--env` when needed
+
+Merge semantics stay exactly what the branch has: dedupe by resolved target
+or name, scalars last-writer-wins, `/dev/null` masking to remove an inherited
+mount. `ramekin config` already labels scopes; it grows the new ones.
+
+`--agent` is the piece with real pull: today comparing pi and claude on the
+same repo means editing a config file back and forth. (It also interacts
+with the shared-base-tag loose end above — per-run agent switching makes the
+tag churn much more visible.)
+
+### 3. Sharing out: jj-backed writable mounts now, outbox if needed
+
+The branch already contains most of the answer. A `claude {}` entry with
+`writable` bind-mounts a file or directory out of the dotfiles *working
+copy* — and crucially, only that path, never the repo metadata. That gives:
+
+- The agent can write improvements directly (skills, CLAUDE.md).
+- The agent cannot touch `.jj`/`.git`, other dotfiles, or history.
+- Every change lands as an ordinary working-copy diff on the host: `jj st`
+ shows it, `jj diff` reviews it, `jj restore` rejects it. Nothing is
+ irreversible.
+
+So the safety model is *review-after with guaranteed rollback*, which for a
+single user is probably the right cost/benefit. Two cheap hardening steps
+make it trustworthy enough to leave on:
+
+- A `ramekin-prompt.md` section telling the agent which paths are shared
+ config and that edits there propagate to the host — so changes are
+ deliberate, not incidental.
+- `ramekin run` warns at startup when a writable mount's source sits in a
+ dirty jj working copy, so agent edits don't get tangled with unrelated
+ uncommitted changes.
+
+If review-*before* ever becomes necessary (or for the pi side, where config
+is copied and in-container edits currently evaporate), the fallback design is
+an **outbox**: a fresh per-session writable mount at `/root/.ramekin/outbox`,
+a prompt instruction to drop proposed config changes there mirroring the
+config-dir layout, and a `ramekin outbox list|diff|apply|discard` subcommand
+that diffs proposals against their known sources and copies them over only on
+explicit apply. Nothing reaches dotfiles without confirmation. The outbox is
+strictly additive — same mount plumbing, no changes to the models above — so
+deferring it costs nothing.
+
+## Sequencing
+
+1. Rebase `claude-code` onto main, fixing the README drift and deciding the
+ base-tag question in the same pass. Merge it — it's the foundation.
+2. `include` + project-local layer (config.rs only, mechanical).
+3. `--agent` CLI override; `--mount`/`--env` if they earn their keep.
+4. Prompt section + dirty-working-copy warning for writable shared mounts.
+5. Outbox, only if review-before turns out to matter in practice.
## Open questions
-- Does pi's `--append-system-prompt` stay, or do both agents move to
- memory-file injection for symmetry?
-- Should `--rebuild`-style flags also become config (e.g. project pins a
- base image)? Not needed yet; the layering makes it cheap to add later.
-- One image with both agents vs. per-agent images: starting with one image;
- revisit if the image bloats or the agents' base requirements diverge.
+- Per-agent base image tags vs. one shared tag (see loose ends). Leaning
+ per-agent tags with the project Dockerfile `FROM` parametrized by build arg.
+- Should pi eventually move to the bind-mount model (a `writable` field on
+ `pi {}` entries) so both agents share outbound semantics? Today pi's
+ copy-assembly means in-container edits are always lost, which makes the
+ outbox more interesting for pi than for claude.