Docs · Configuration

Where everything lives.

Poddle keeps state in plain files under your OS config directory, resolves templates from three predictable places, and reads a short list of environment variables. No hidden global state.

On-disk layout

Everything is a file you can read, back up, or check in. Secrets are stored mode 0600 and never leave your machine - the broker seals them into its vault at spin-up.

WhatPathNotes
Identities~/.config/poddle/identities/<name>/One directory per login - meta.toml plus the sealed token, mode 0600.
User templates~/.config/poddle/templates/<name>.tomlYour personal blueprints, available in any project.
Connectors~/.config/poddle/connectors/Brokered service definitions added with poddle connect.
Agent config~/.config/poddle/harness/<harness>/A pod agent's own native config (settings, plugins, MCP servers) - seeded into the pod and persisted.
Project templates.poddle/<name>.tomlChecked into the repo and shared with the team.
Project default.poddle.tomlAuto-applied when you run poddle up with no --template.

OS-correct paths

~/.config/poddle is the Linux/macOS location; on Windows it resolves to %AppData%\poddle. Poddle uses the platform config dir, so you don’t hard-code paths.

How a template is chosen

When you pass --template <name>, poddle looks it up by filename across two locations. A project file shadows a user file of the same name, so a repo can pin its own version of a shared blueprint:

  1. Project - .poddle/<name>.toml (wins on a name clash).
  2. User - ~/.config/poddle/templates/<name>.toml.

With no --template, poddle applies the project default - the repo’s root .poddle.toml - if one exists. Missing files are never an error; you only need what you use.

How extends merges

A template can extends one parent or a list of them; chains flatten and a cycle is a hard error. When a child layers over a parent, values combine by type:

Field kindRuleExamples
ScalarsChild overrides - a non-empty child value wins.image, size, harness, identity, repo, secret_scan, egress
ListsAppend - parent items first, then child.setup, scripts, connectors, block_paths, mounts
MapsMerge - child keys override same-named parent keys.env
FlagsSticky - once a parent turns it on it stays on.autoscale

One base, many specializations

Because lists append and scalars override, a shared base that sets safe defaults (image, secret_scan, egress) is inherited by every template that extends it - a project template only states what differs. See the Templates examples for the pattern.

Customizing your agent config

Each pod-side agent reads its own native config file. Drop yours under~/.config/poddle/harness/<harness>/ and poddle seeds it into the pod's config directory on up, then persists that directory as a named volume so it survivesmove. Poddle's broker wiring rides a separate side channel and never writes - or overwrites - these files, so the agent's full native config (settings, plugins, MCP servers) is yours to own.

HarnessConfig fileWhat goes there
codexcodex/config.tomlCodex's own settings and its [mcp_servers] block.
opencodeopencode/opencode.jsonopencode's settings, plugins, and its mcp block.
pipi/mcp.json (+ extensions/)pi's MCP servers and extensions. models.json is poddle-owned.
claude-code~/.claude.json (mcpServers)Preserved, not overwritten - poddle merges only the onboarding flag. A project .mcp.json in the repo persists too.
aider.aider.conf.yml (in the repo)A project config in /workspace, which already persists - no host seed needed.

For example, to give Codex a filesystem MCP server scoped to the workspace:

>_~/.config/poddle/harness/codex/config.toml
# Your Codex settings and MCP servers. Poddle injects its broker provider via
# -c flags on each `codex exec`, so it never writes or overwrites this file.

[mcp_servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]

opencode uses the same idea under an mcp key in its JSON config:

>_~/.config/poddle/harness/opencode/opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "filesystem": {
      "type": "local",
      "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
    }
  }
}

Codex provider config is user-scope only

Codex only reads provider keys (model_provider, model_providers) from itsuser-scope config.toml - a project .codex/config.tomlis ignored for provider settings. Keep any custom provider config in the seeded, user-scope file. Poddle's own provider override rides -c flags on every codex exec, so it never touches this file and won't collide with yours.

A local stdio MCP server runs inside the pod. One bundled into the image works as-is; one that fetches at launch - the npx above pulls from npm - needs its registry in the pod's egress allow-list, which codex and opencode already permit by default. A remote MCP server can be brokered instead, so its token never lands in the pod - see Brokered MCP below.

For claude-code, poddle merges hasCompletedOnboarding into ~/.claude.jsonrather than overwriting it, so your user-scope MCP servers survive a poddle task. (Seeding the home-level ~/.claude.json from the host dir is a follow-up; ~/.claude/and a project .mcp.json already persist.)

aider's config is a project file - .aider.conf.yml - in the repo, and /workspaceis already a persisted volume, so it persists with no extra setup.

Brokered MCP

A remote MCP server (Streamable HTTP) can be wired in the same way as a git host or registry: the token is sealed in the broker's vault, never in the pod. Add it as a connector, same as any other service:

>_zsh
# pipe the PAT on stdin so it never hits your shell history
echo $MCP_TOKEN | poddle connect add search \
  --connector mcp --url https://mcp.example.com/mcp
✓ connection “search” added (brokered)

Then opt a pod into it the same way as any connector - list it in the template'sconnectors. On poddle up, poddle issues a pod-scoped handle for the token, sets it in an env var, and registers the MCP server with the agent through the agent's own channel - pointed at the broker's gateway instead of the real endpoint. The agent presents the handle on every request; the broker swaps in the real token and relays to the server (both the POST request/response leg and the server-initiated SSE stream). The real token never enters the pod, and poddle down revokes it immediately.

HarnessStatus
codexWired today - codex mcp add registers the brokered server with a bearer-token env var.
claude-codeWired today - claude mcp add registers the brokered server, scoped to the user.
opencodeWired today - the brokered server is merged into the OPENCODE_CONFIG layer.
piWired today - pi has no built-in MCP, so poddle installs the third-party pi-mcp-adapter extension and registers the brokered server in mcp.json.
geminiWired today - poddle merges the brokered server (an httpUrl entry) into ~/.gemini/settings.json; the handle rides the auth header as an env-var reference, expanded at load so it never lands on disk.
aiderNot supported - aider has no MCP client (no --mcp-server in any release; MCP is an open, unmerged proposal). Nothing for poddle to wire.

MCP servers protected by OAuth 2.1 work too - just omit --token and let poddle probe the URL:

>_zsh
# no --token: poddle probes the URL, sees a 401, and opens your browser
poddle connect add search --connector mcp --url https://mcp.example.com/mcp
added search (OAuth)

On a 401, poddle discovers the server's authorization server metadata and, if the server supports Dynamic Client Registration, registers a client on the spot. Servers without DCR need a pre-registered client: pass --client-id (and --client-secret for a confidential client). Either way, consent happens in your browser on the host - the pod never sees it. The broker holds the access and refresh tokens in its vault and refreshes the access token automatically as it nears expiry; the pod only ever holds a revocable handle, exactly as with a bearer-token connection. If the provider rotates its refresh token, poddled writes the rotation back to disk and the host reconciles it into the connection's stored material on the nextpoddle up, so a restart no longer forces a re-consent in the normal case. poddle connect reauth <name> remains the fallback for a grant that's actually been revoked or expired: it re-runs browser consent and replaces the stored tokens. poddle daemon status lists any connection that needs it.

For a headless or remote host with no browser - CI, an SSH box, a bare server - add--device to either command: poddle connect add search --connector mcp --url ... --device or poddle connect reauth search --device runs the OAuth 2.0 device authorization grant (RFC 8628) instead of opening one. Poddle prints a short user code and a verification URL; open that URL on any device, enter the code, and poddle polls until you approve - then seals the same access and refresh tokens as the browser flow. The browser flow remains the default when --device is omitted, and the device flow only works if the server's authorization server advertises a device_authorization_endpoint.

Rotated refresh tokens write back automatically

When the broker refreshes a token and the provider rotates the refresh token, poddledpersists the rotation right away. The host picks it up and reconciles it into the connection's stored OAuth material the next time you run poddle up (newest wins). Write-back is best-effort - a persist failure never blocks a request or an up - so it covers the common case, not every case. If a grant is genuinely revoked, run poddle connect reauth <name> to recover.

Explicit policies don't auto-add the MCP host

With no explicit policy, poddle derives a default-deny allow-list and adds the MCP server's host to it automatically. Under an explicit named policy, poddle uses yourallow_upstreams as-is and does not merge connector hosts into it - add the MCP host yourself (see Governance).

This covers remote Streamable HTTP servers only. A local stdio MCP server that needs a secret of its own is a separate, unbuilt mechanism.

Environment variables

A handful of variables tune the CLI and daemon. All are optional - the defaults are the common case.

VariableDefaultEffect
PODDLE_HOSTlocal podmanTarget a remote host as ssh://user@host - the same commands run against it. Empty means local Podman.
PODDLE_BROKER_ADDRhost.containers.internalThe routable address a remote pod dials to reach the broker (e.g. this machine’s LAN IP). Local pods use the default.
PODDLE_ANTHROPIC_BASE_URLhttps://api.anthropic.comPoint the Anthropic provider at a compatible proxy or gateway instead of the public API.
PODDLE_OPENAI_BASE_URLhttps://api.openai.comPoint the OpenAI provider at a compatible proxy or gateway instead of the public API.
PODDLE_GOOGLE_BASE_URLhttps://generativelanguage.googleapis.comPoint the Google (Gemini) provider at a compatible proxy or gateway instead of the public API.
PODDLE_AUTOSCALE_INTERVAL15sHow often the daemon’s autoscaler samples memory pressure (a Go duration like 5s or 1m).
>_zsh
# run the same commands against a remote host
export PODDLE_HOST=ssh://you@build-box
poddle up my-sandbox --identity work