Configuration

Updated Jul 27, 2026

Configuration can be provided via:

  1. Command-line arguments (highest precedence)
  2. Environment variables
  3. Configuration file
  4. Built-in defaults (lowest precedence)

Configuration file

Configuration files are discovered in the following order (first match wins):

  1. Path specified via --config CLI argument
  2. Path specified via PODBOT_CONFIG_PATH environment variable
  3. The XDG Base Directory Specification configuration directory ($XDG_CONFIG_HOME/podbot/config.toml, typically ~/.config/podbot/config.toml)
  4. ~/.podbot.toml (dotfile in home directory)

If --config is provided, PODBOT_CONFIG_PATH is ignored. If the --config path does not exist, podbot falls back only to the remaining file-based discovery locations ($XDG_CONFIG_HOME/podbot/config.toml and ~/.podbot.toml) and does not consult PODBOT_CONFIG_PATH.

Note: GitHub App credentials (app_id, installation_id, private_key_path) are validated only when GitHub operations are performed. Commands like podbot ps or podbot stop do not require GitHub configuration.

# Container engine socket (Podman or Docker)
# Unix socket (default for local daemons):
engine_socket = "unix:///run/user/1000/podman/podman.sock"
# TCP endpoint (for remote daemons):
# engine_socket = "tcp://docker.example.com:2375"

# Container image for the sandbox
image = "ghcr.io/example/podbot-sandbox:latest"

[github]
# GitHub App credentials (optional, for private repositories)
app_id = 12345
installation_id = 67890
private_key_path = "/home/user/.config/podbot/github-app.pem"

[sandbox]
# Run the container in privileged mode (less secure, more compatible)
privileged = false
# Mount /dev/fuse for fuse-overlayfs support (required for inner Podman)
mount_dev_fuse = true
# SELinux label handling: "disable_for_container" or "keep_default"
selinux_label_mode = "disable_for_container"

[agent]
# Default agent type: "claude", "codex", or "custom"
kind = "claude"
# Execution mode for the agent: "podbot", "codex_app_server", or "acp"
mode = "podbot"
# Custom launcher command (required when kind = "custom")
command = "opencode"
# Additional launcher arguments (defaults to [])
args = ["acp"]
# Environment variables copied from the host (defaults to [])
env_allowlist = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]

[workspace]
# Workspace source: "github_clone" or "host_mount"
source = "github_clone"

# Exact clone destination for github_clone workspaces
base_dir = "/work"
# Host path mounted into the sandbox when source = "host_mount"
host_path = "/abs/path/to/project"
# Container path for the mount; defaults to "/workspace" in host_mount mode
container_path = "/workspace/project"

[creds]
# Copy credentials from the host into the container
copy_claude = true
copy_codex = true

[mcp]
# HTTP bridge reachability strategy for hosted MCP servers
bind_strategy = "host_gateway"
# Idle timeout in seconds for hosted MCP bridges
idle_timeout_secs = 900
# Maximum message size in bytes
max_message_size_bytes = 1048576
# Token issuance policy: "per_workspace" or "per_wire"
auth_token_policy = "per_workspace"
# Allowed-origin policy: "same_origin" or "any"
allowed_origin_policy = "same_origin"

Semantic validation rules:

  • podbot run accepts only agent.mode = "podbot".
  • podbot host remains temporarily disabled in this release, even though the configuration model still reserves agent.mode = "codex_app_server" and "acp" for the hosted protocol path.
  • agent.kind = "custom" requires a non-empty agent.command.
  • Built-in agent kinds reject agent.command and agent.args.
  • workspace.source = "host_mount" requires workspace.host_path and defaults workspace.container_path to "/workspace" when omitted.
  • workspace.source = "github_clone" rejects host-mount-only fields.

For podbot run, --repo must use GitHub owner/name form, such as leynos/podbot. The --branch flag is required and has no default; Podbot does not guess the default branch. When workspace.source = "github_clone", Podbot clones that repository directly into workspace.base_dir inside the container and treats that path as the workspace root.

Private key file requirements

The private_key_path field must point to a PEM-encoded RSA private key. GitHub App authentication uses the RS256 algorithm exclusively, so only RSA keys are supported.

Accepted formats:

  • PKCS#1: header -----BEGIN RSA PRIVATE KEY-----
  • PKCS#8: header -----BEGIN PRIVATE KEY----- (must contain an RSA key)

To generate a suitable key:

openssl genrsa -out github-app.pem 2048

Common error messages when loading the key:

Message Cause
"file is empty" The key file exists but contains no data.
"failed to read file" The file does not exist or cannot be read.
"the file appears to contain an ECDSA key" An EC key was provided instead of RSA.
"the file appears to contain an OpenSSH-format key" An OpenSSH key was provided; convert with ssh-keygen -p -m pem -f <keyfile>.
"invalid RSA private key" The file contents are not valid PEM-encoded RSA data.

Credential validation errors

After loading the private key successfully, podbot validates the credentials against the GitHub API by making an authenticated request to GET /app. If this validation fails, podbot classifies the failure mode and provides actionable error messages with remediation hints:

Message fragment Cause and remediation
"credentials rejected (HTTP 401)" The private key does not match the App, or the App has been suspended. Verify the App ID and regenerate the private key from the GitHub App settings page. Check for clock skew.
"insufficient permissions (HTTP 403)" The App lacks required permissions. Check the App's permission settings in GitHub.
"App not found (HTTP 404)" The App ID is incorrect or the App has been deleted. Verify that github.app_id is correct.
"GitHub API unavailable (HTTP 5xx)" GitHub is experiencing an outage or maintenance. Check https://www.githubstatus.com for service status. Retry after the service recovers.
"failed to validate GitHub App credentials" A network error occurred or the API returned an unexpected status. Check network connectivity and DNS resolution. Review the detailed error message for the specific cause.

Installation token acquisition

For workspace.source = "github_clone", podbot acquires a short-lived GitHub App installation access token on the host before later clone and fetch work. The token is scoped by the GitHub App installation and expires after about one hour. Podbot keeps the GitHub App private key on the host; later token-file work will expose only the short-lived installation token to the sandbox.

Token acquisition uses a refresh buffer so podbot can schedule renewal before expiry. The token string is available to Git credential delivery, but logs and debug output include only non-secret timing metadata such as acquisition time, derived expiry time, refresh time, and buffer duration.

Common token acquisition errors:

  • "failed to acquire installation token": GitHub rejected token acquisition, the installation ID is wrong, the App installation is unavailable, or the network failed.
  • "invalid token expiry buffer": the configured buffer could not be converted for the GitHub client. Use a smaller positive buffer.

Environment variables

All configuration options can be set via environment variables using the PODBOT_ prefix:

Variable Configuration key
PODBOT_ENGINE_SOCKET engine_socket
PODBOT_IMAGE image
PODBOT_GITHUB_APP_ID github.app_id
PODBOT_GITHUB_INSTALLATION_ID github.installation_id
PODBOT_GITHUB_PRIVATE_KEY_PATH github.private_key_path
PODBOT_SANDBOX_PRIVILEGED sandbox.privileged
PODBOT_SANDBOX_MOUNT_DEV_FUSE sandbox.mount_dev_fuse
PODBOT_SANDBOX_SELINUX_LABEL_MODE sandbox.selinux_label_mode
PODBOT_AGENT_KIND agent.kind
PODBOT_AGENT_MODE agent.mode
PODBOT_AGENT_COMMAND agent.command
PODBOT_AGENT_ARGS agent.args
PODBOT_AGENT_ENV_ALLOWLIST agent.env_allowlist
PODBOT_WORKSPACE_SOURCE workspace.source
PODBOT_WORKSPACE_BASE_DIR workspace.base_dir
PODBOT_WORKSPACE_HOST_PATH workspace.host_path
PODBOT_WORKSPACE_CONTAINER_PATH workspace.container_path
PODBOT_CREDS_COPY_CLAUDE creds.copy_claude
PODBOT_CREDS_COPY_CODEX creds.copy_codex
PODBOT_MCP_BIND_STRATEGY mcp.bind_strategy
PODBOT_MCP_IDLE_TIMEOUT_SECS mcp.idle_timeout_secs
PODBOT_MCP_MAX_MESSAGE_SIZE_BYTES mcp.max_message_size_bytes
PODBOT_MCP_AUTH_TOKEN_POLICY mcp.auth_token_policy
PODBOT_MCP_ALLOWED_ORIGIN_POLICY mcp.allowed_origin_policy

PODBOT_AGENT_ARGS and PODBOT_AGENT_ENV_ALLOWLIST use comma-separated values.

Container engine socket

The socket endpoint for connecting to Docker or Podman is resolved in the following order (first match wins):

  1. --engine-socket CLI argument
  2. engine_socket in configuration file
  3. PODBOT_ENGINE_SOCKET environment variable
  4. DOCKER_HOST environment variable
  5. CONTAINER_HOST environment variable
  6. PODMAN_HOST environment variable
  7. Platform default (unix:///var/run/docker.sock on Unix, npipe:////./pipe/docker_engine on Windows)

This allows podbot to integrate with existing Docker and Podman environments without additional configuration. When DOCKER_HOST or PODMAN_HOST is already set for container tooling, podbot will automatically use that endpoint.

TCP endpoint support

In addition to Unix sockets and Windows named pipes, podbot supports TCP connections to remote container engines. This is useful when the Docker or Podman daemon is running on a different host or is configured to listen on a TCP port.

Supported TCP endpoint formats:

Format Example Notes
tcp://host:port tcp://192.168.1.100:2375 Rewritten internally to http://
http://host:port http://docker.example.com:2375 Used directly
https://host:port https://docker.example.com:2376 TLS-encrypted connection

Configuration examples:

Via CLI argument:

podbot run --engine-socket tcp://remotehost:2375 --repo owner/name --branch main

Via environment variable:

export DOCKER_HOST=tcp://192.168.1.100:2375
podbot run --repo owner/name --branch main

Via configuration file:

engine_socket = "tcp://docker.example.com:2375"

TCP-specific troubleshooting:

Error Cause Resolution
failed to connect to container engine: <message> TCP endpoint unreachable or daemon not listening Verify the remote host is reachable and the daemon is configured to listen on the specified port
container engine health check failed: <message> Connection established but daemon did not respond Verify the daemon is healthy: curl http://remotehost:2375/v1.40/_ping
container engine health check timed out after 10 seconds Network latency or daemon overloaded Check network connectivity and daemon load

Security note: TCP connections without TLS (tcp:// and http://) transmit data unencrypted. Use https:// with TLS certificates for production environments. Consult the Docker or Podman documentation for configuring TLS.

Engine health check

When connecting to a container engine, podbot performs a health check to verify the engine is responsive. This confirms the engine is operational, not just that the socket is reachable.

Health check behaviour:

  • A ping request is sent to the engine after establishing the connection
  • The check times out after 10 seconds if the engine does not respond
  • If the health check fails, podbot reports a clear error message

Possible error messages:

Error Cause
container engine health check failed: <message> The engine did not respond correctly to the ping request
container engine health check timed out after 10 seconds The engine took too long to respond

Connection error troubleshooting

When podbot cannot connect to the container engine, it provides actionable error messages to help diagnose the issue.

Possible connection errors:

Error Cause Resolution
permission denied accessing container socket: <path> User lacks permission to access the Docker/Podman socket Add user to the docker group: sudo usermod -aG docker $USER && newgrp docker. For Podman, use the rootless socket at /run/user/$UID/podman/podman.sock (where user ID (UID) identifies the account)
container engine socket not found: <path> Socket file does not exist; daemon not running Start the daemon: Docker: sudo systemctl start docker. Podman: systemctl --user start podman.socket
failed to connect to container engine: connection refused Daemon not accepting connections Restart the daemon service and check its status

Common permission scenarios:

  • Docker on Linux: By default, the Docker socket (/var/run/docker.sock) is owned by the docker group. Add the current user to this group:
   sudo usermod -aG docker $USER
   newgrp docker  # Apply group membership without logging out
  • Rootless Podman: Use the user-level socket instead of the system socket:
   # Start the user socket
   systemctl --user start podman.socket

   # Configure podbot to use it
   export PODBOT_ENGINE_SOCKET="unix:///run/user/$(id -u)/podman/podman.sock"
  • Podman with sudo: If using the system Podman socket, ensure the socket service is running:
   sudo systemctl start podman.socket

Sandbox configuration

The [sandbox] section controls the security and compatibility trade-offs for the container environment.

Setting Default Description
privileged false Run container in privileged mode
mount_dev_fuse true Mount /dev/fuse for fuse-overlayfs support
selinux_label_mode "disable_for_container" SELinux label handling mode

Minimal mode (default): privileged = false, mount_dev_fuse = true

This is the recommended configuration for most users. It provides:

  • Better security isolation by avoiding the privileged flag
  • Support for inner Podman via fuse-overlayfs
  • Compatibility with most Podman-in-Podman workflows

Privileged mode: privileged = true

Enable privileged mode only when minimal mode does not work for the target environment. Privileged mode:

  • Provides maximum compatibility with nested container operations
  • Expands the container's attack surface significantly
  • Should be avoided unless specifically required
  • Ignores mount_dev_fuse because the engine grants full device access in privileged mode

Disabling /dev/fuse: mount_dev_fuse = false

The /dev/fuse mount is required for fuse-overlayfs, which enables inner Podman to function correctly. Disable this only when the agent container does not need nested container support.

SELinux label mode: selinux_label_mode

Controls how SELinux labels are applied to the container process:

  • "disable_for_container" (default): Applies SecurityOpt = ["label=disable"] so rootless nested Podman workflows do not fail under strict SELinux labelling. This is the recommended setting for most environments.
  • "keep_default": Leaves SELinux labelling at engine defaults. Use this when the host SELinux policy is already configured to permit nested container operations, or when SELinux enforcement is disabled system-wide.

In privileged mode, this setting is ignored because the engine governs security labelling directly.

Container creation behaviour

When podbot creates a sandbox container, it applies the following host security settings:

  • privileged = true: sets HostConfig.Privileged = true and uses engine defaults for capabilities, devices, and SELinux options. The mount_dev_fuse and selinux_label_mode settings are ignored.
  • privileged = false with the default selinux_label_mode: sets HostConfig.Privileged = false and applies SecurityOpt = ["label=disable"].
  • privileged = false and selinux_label_mode = "keep_default": sets HostConfig.Privileged = false without adding SecurityOpt, leaving SELinux labelling at engine defaults.
  • mount_dev_fuse = true (in non-privileged mode): additionally maps /dev/fuse and adds SYS_ADMIN capability so fuse-overlayfs can run.
  • mount_dev_fuse = false (in non-privileged mode): skips /dev/fuse mapping and capability additions.

Container creation requires image to be configured. If it is missing or whitespace-only, podbot returns:

missing required configuration: image

Podbot resolves this image from layered configuration precedence (--image, then PODBOT_IMAGE, then file/default values). Validation occurs before the engine create call, so no container-create request is sent when the resolved image is empty.

Credential injection behaviour

At sandbox startup, podbot can copy host agent credentials into the container filesystem using a tar upload to /root.

  • creds.copy_claude = true selects ~/.claude.
  • creds.copy_codex = true selects ~/.codex.
  • Selected directories that are missing are skipped.
  • If nothing is selected or present, credential injection succeeds as a no-op and no upload request is sent.
  • Host-side selection or archive-build failures are reported as FilesystemError::IoError.
  • Daemon upload failures are reported as ContainerError::UploadFailed.

When credentials are uploaded, expected container paths are:

  • /root/.claude for Claude credentials.
  • /root/.codex for Codex credentials.

Permission bits from source files and directories are preserved in the uploaded tar entries.

Verification notes:

  1. Start a sandbox with the desired copy_claude and copy_codex settings.
  2. Check which directories exist in the container:
   podbot exec <container> -- ls -la /root
  1. Compare permission bits for a representative file between host and container, for example, with stat on each side.