<!--
Sitemap:
- [Installation](/installation)
- [Upgrading](/upgrading): Version-specific steps for upgrading an existing Bento install.
- [Concepts](/concepts)
- [Build your first pipeline](/tutorials/pipeline-args)
- [Target a specific issue or PR from a URL](/tutorials/url-targeting)
- [Keep state across runs](/tutorials/pipeline-state)
- [Fire a pipeline on a schedule or on demand](/tutorials/schedule-and-fire)
- [Configuration](/configuration)
- [Knowledge Base](/knowledge-base/)
- [Method & delivery](/knowledge-base/modes)
- [Config](/knowledge-base/config)
- [MCP](/knowledge-base/mcp)
- [Pipeline configuration reference](/pipelines/config)
- [Filters](/pipelines/filters)
- [Triggers](/triggers/)
- [GitHub Trigger](/triggers/github)
- [Linear Trigger](/triggers/linear)
- [Webhook](/triggers/webhook)
- [Schedule Trigger](/triggers/schedule)
- [Manual Trigger](/triggers/manual)
- [Traces](/pipelines/traces)
- [Slack](/integrations/slack)
- [Public Access](/public-access)
- [Context Engineering](/context-engineering)
- [Best Practices](/best-practices)
- [Troubleshooting](/troubleshooting)
- [Architecture](/architecture/vision)
- [Workspaces](/workspaces)
- [Authentication](/authentication)
- [Identity](/identity)
- [Security](/security)
- [References](/references)
- [Changelog](/changelog): Bento release history.
- [CLI Reference](/cli/)
- [Setup](/cli/setup)
- [Lifecycle](/cli/lifecycle)
- [Sandbox Image](/cli/image)
- [Observability](/cli/observability)
- [Diagnostics](/cli/diagnostics)
- [Triggers](/cli/triggers)
- [Workbench](/cli/workbench)
- [Auth](/cli/auth)
- [Knowledge](/cli/knowledge)
- [Bento](/index)
- [Runtime Wrapper](/architecture/runtime-wrapper)
- [Skill Evolve](/architecture/skill-evolve)
-->

# Configuration

`bento init` creates two config files in your project directory:

* **`.bento/daemon.yaml`** — daemon-wide settings
* **`.bento/pipelines/*.yaml`** — one file per pipeline (see [Pipeline Config](/pipelines/config))

Runtime state lives under `~/.bento/<name>/` and is never committed. Environment variables expand with `${VAR}` syntax in both files.

***

## Identity

```yaml
name: bento
```

Sets the launchd/systemd service name and the runtime directory (`~/.bento/<name>/`). Must be unique per machine when running multiple daemon instances.

***

## Port

```yaml
port: 7890
```

The port for the daemon's HTTP server, which serves both webhooks and the CLI API. CLI commands read this from the project config automatically, so each project's daemon can run on its own port with no flags. Defaults to `7890`. Supersedes the legacy `webhooks.port`.

***

## GitHub

```yaml
github:
  username: your-org-agent
```

Sets the GitHub Agent principal used by daemon API calls, post-backs, git attribution, and actor runs. `username` is required when any pipeline has a GitHub trigger, GitHub output, or repo-backed checkout, and when the top-level `repos` map is non-empty. Authenticate it with `bento setup gh` or `bento setup gh --env AGENT_GITHUB_TOKEN`; credentials live under `~/.bento/<name>/gh`. On a dedicated daemon host, add `--host` to authenticate the machine's default `gh` profile too.

This is the single-principal compatibility form. Pipelines can instead declare [principal bindings](/identity#principal-bindings) and select a canonical GitHub principal at each actor boundary. The top-level `repos` HTTP API still requires `github.username` because it has no pipeline execution boundary.

***

## Database

```yaml
database:
  url: postgresql://bento:bento@localhost:8421/bento
```

PostgreSQL connection string. **Required** — the daemon refuses to boot without it, and there is no built-in default (a silent fallback could land two daemons on the same database). Note that an unset `${VAR}` reference expands to empty, which counts as unset. The quickstart Compose file starts Postgres on port `8421` with the credentials shown above.

***

## Webhooks

```yaml
webhooks:
  host: 127.0.0.1
  secret: ${GITHUB_WEBHOOK_SECRET}
```

The daemon listens for inbound webhook payloads here (on the root `port:` above). `secret` is used for HMAC verification of GitHub payloads — set it to the same value you configure in your GitHub webhook settings.

***

## Targets

```yaml
targets:
  claude-high:
    runtime: claude
    model: claude-opus-5
    effort: high
    credential_pool: [max-0x, max-1x, max-2x]
  claude-backup:
    runtime: claude
    model: claude-sonnet-5
    credential: max-0x
  codex-high:
    runtime: codex
    model: gpt-5.6-sol
    effort: high
    credential: oai-personal
```

Defines reusable execution targets. Each target binds a runtime, model, optional reasoning effort, and credential selection. `credential` selects one fixed named credential. `credential_pool` selects a non-empty ordered list of unique credentials. Pool entries may mix subscription and API-key credentials. A target cannot set both fields.

Pipelines, the orchestrator, capabilities, MCP and HTTP invocations, `bento ask`, the formatter, and knowledge rewrite reference targets. On quota or authentication failure, Bento tries each credential in a target's pool within the same run before advancing to the next target in the route. A Claude session-limit result cools that credential down until its reported UTC reset time.

Capability definitions may declare an ordered `targets` list. If omitted, the capability inherits the parent invocation route.

## Orchestrator

```yaml
orchestrator:
  targets: [claude-high, claude-backup]
```

Sets ordered target defaults for pipelines that explicitly declare `orchestrator.strategy.type: auto`. This block does not enable orchestration by itself. A pipeline that omits its own `orchestrator` field spawns its named agent directly. There is no built-in target default; an opted-in pipeline requires this block. The in-process orchestrator currently supports targets whose runtime is `claude`. Fallback targets are tried before any specialist work has been dispatched and share the invocation timeout.

The orchestrator target is independent of each pipeline's specialist targets.

### tools

Operator-defined tools exposed to agents via the daemon's MCP server, each registered as `orchestrator_<name>`:

```yaml
orchestrator:
  targets: [claude-high]
  tools:
    deploy_status:
      description: Check deploy status for a service
      input:
        service: { type: string, required: true }
      run: ./scripts/deploy-status.sh $service
```

| Field | Description |
|-------|-------------|
| `description` | Tool title shown to the agent. |
| `input` | Parameter map. Each entry has `type` (`string` | `number` | `boolean`), optional `required`, optional `default`. |
| `run` | Shell command template, executed on the daemon host with a 120 s timeout. Input values substitute as `$name`. |

***

## Formatter

```yaml
formatter:
  target: claude-high
```

Runs an optional model pass over structured `reply_body` output before posting it. Omit the block to post the extracted body unchanged.

***

## Ask

```yaml
ask:
  target: claude-high
```

Sets the execution target used by model-backed `bento ask` invocations. When omitted, those invocations require an explicit `--target` flag. `bento ask --fast` does not run a model and cannot use `--target`.

| Field | Description |
|-------|-------------|
| `target` | Named target from `targets:`. |

***

## Credentials

```yaml
credentials:
  claude:
    max-0x:
      kind: subscription
      env: CLAUDE_CODE_OAUTH_TOKEN
    claude-api:
      kind: api_key
      env: ANTHROPIC_API_KEY_WORK
  codex:
    oai-personal:
      kind: subscription
      path: ${HOME}/.codex/auth.json
    openai-api:
      kind: api_key
      env: OPENAI_API_KEY_WORK
```

Defines named model-provider accounts referenced by targets. Credential names are unique across runtimes. Secrets remain in environment variables or auth files; configuration stores only their source names or paths.

| Runtime / kind | Source | Delivered as |
|----------------|--------|--------------|
| `claude` / `subscription` | `env` | `CLAUDE_CODE_OAUTH_TOKEN` |
| `claude` / `api_key` | `env` | `ANTHROPIC_API_KEY` |
| `codex` / `subscription` | `path` | Per-run `auth.json` in `CODEX_HOME` |
| `codex` / `api_key` | `env` | `CODEX_API_KEY` for the `codex exec` invocation |

Every run receives only its selected provider credential. Other native provider credential variables are removed from that run's environment. Daytona receives path-backed Codex authentication through a per-run remote `CODEX_HOME`; the source file is not mounted.

`api` remains accepted as an alias for existing configurations.

The registry is frozen at daemon startup. Restart after editing it. A SIGHUP reload validates pipeline references against the registry currently running and warns that registry edits require restart.

***

## Defaults

```yaml
defaults:
  guardrails:
    timeout: 900
  knowledge:
    method: none
    delivery: browse
    filter:
      tags: [conventions]
  sandbox:
    backend: docker
  setup:
    - run: ./scripts/index-symbols.sh
  output:
    github: comment
```

Global guardrails applied to every pipeline unless the pipeline overrides them. Fields are merged per-key — a pipeline's `guardrails:` block wins where both set the same field. See [Pipeline guardrails](/pipelines/config#guardrails) for the field reference.

`defaults.knowledge` works the same way for knowledge injection (`filter` replaces wholesale rather than merging). See [Pipeline knowledge](/pipelines/config#knowledge) for the `method`/`delivery` fields.

`defaults.sandbox`, `defaults.setup`, and `defaults.output` supply shareable defaults for pipelines on the same daemon that would otherwise repeat the same backend/image, setup steps, or output sink. `sandbox` merges per-key with the pipeline's block winning each key (e.g. a default `backend` with a pipeline `image`). A pipeline's `setup:` or `output:` replaces the corresponding default wholesale; `output: {}` disables inherited post-back. See [sandbox](/pipelines/config#sandbox), [setup](/pipelines/config#setup), and [output](/pipelines/config#output) for the field references.

***

## Queue

```yaml
queue:
  concurrency: 2
  retry:
    attempts: 3
  circuit_breaker:
    failure_threshold: 5
    escalate:
      webhook: https://ops.example.com/bento-alerts
```

Controls the job queue. `concurrency` sets how many agent runs execute in parallel. The circuit breaker opens after `failure_threshold` consecutive failures and pauses new runs until manually cleared with `bento queue resume`.

`circuit_breaker.escalate.webhook` POSTs `{"message": "..."}` as JSON to the given URL when the breaker opens. The schema also declares `escalate.slack` and `escalate.telegram`, but neither is implemented — configuring them logs a warning and sends nothing.

***

## Workspaces

```yaml
workspaces:
  checkoutRetentionDays: 30
  transcriptRetentionDays: 90
  closedRetentionDays: 14
```

Retention tiers for stored workspaces (BIP-5). Once a workspace is idle past
`checkoutRetentionDays` (default 30), its regenerable `checkout/` is reaped;
run transcripts and `notes.jsonl` survive on the longer `transcriptRetentionDays`
tier (default 90, clamped up to at least `checkoutRetentionDays` so a transcript
never expires before the checkout it describes) and
the whole workspace is removed only past that window. Independent of idleness,
once a workspace's PR closes (stamped by the `system/workspace-closed`
pipeline), the whole workspace is removed `closedRetentionDays` (default 14)
after the close.

***

## Triggers

```yaml
triggers:
  retention:
    discardedDays: 14
    completedDays: 90
```

Retention windows for stored trigger rows, applied by a boot-time sweep.
Discarded rows (webhook noise that matched no pipeline) are deleted after
`discardedDays` (default 14); terminal rows (`done`, `error`, `timeout`,
`superseded`) after `completedDays` (default 90). Non-terminal rows are never
deleted. Values must be nonnegative numbers of days — invalid values fail the
sweep instead of deleting. Rows keep their full payload until deletion, so a
discarded trigger stays replayable (`bento trigger replay --ignore-filter`)
inside the window.

***

## Tokens

```yaml
tokens:
  retention:
    revokedDays: 30
```

Retention window for revoked token rows, applied hourly by the bundled
`system/revoked-token-sweep` action pipeline.
The daemon rotates its internal `sandbox-mcp` token on every restart and each
rotation revokes the previous row, so without a sweep the `tokens` table grows
unbounded and `bento token list` fills with dead rows. Revoked rows are deleted
`revokedDays` (default 30) after revocation; active rows are never deleted, and
a token stays on the table for the window after revocation so it is still
explicable during an incident. The value must be a nonnegative number of days —
an invalid value fails the sweep instead of deleting.

***

## Repos

```yaml
repos:
  my-project:
    url: acme/my-project
    branch: main
    sandbox:
      image: devcontainer
```

Repo entries used by schedule pipelines that require a checkout. A pipeline's `trigger.repo` field is matched against each entry's `url` (`owner/name`) — the map key (`my-project`) is a label only and is never matched.

`sandbox.image` sets the image the agent runs in for this repo, overriding the daemon default (`agent:default`). A pipeline's own [`sandbox.image`](/pipelines/config#sandbox) still wins over it. The value takes one of three forms:

| Form | Example | Meaning |
|------|---------|---------|
| `devcontainer` | `devcontainer` | Use the repo's own `.devcontainer/devcontainer.json` (or root `.devcontainer.json`). The `image` field is used directly; `build.dockerfile` is built locally. Errors if the repo has none. |
| `./` path | `./infra/Dockerfile`, `./svc/.devcontainer/` | Build from a repo-relative Dockerfile (its directory is the build context), or apply devcontainer semantics rooted at the named `.devcontainer/` directory. |
| image reference | `ghcr.io/acme/dev:latest` | Use a prebuilt image as-is. |

Repo-derived forms (`devcontainer`, `./` paths) resolve after the clone: setup runs in the default image, then the agent runs in the resolved one. They require a local container backend (docker/podman) — on remote backends (daytona) use an image reference.

The image must satisfy the agent-image contract (see `packages/images/agent-default/Dockerfile`): ship the agent CLI the pipeline runs (`claude` / `codex`), and — because runs execute as `--user <host-uid>:0` — declare an `ENV HOME` pointing at a group-0-writable directory (`chown -R 0:0 $HOME && chmod -R g=u $HOME`). Base custom images on `agent:default`, or replicate both in the Dockerfile.

***

## Lists

```yaml
lists:
  watch-repos:
    - vercel/eve
    - vercel/next.js
```

Named reusable lists of strings. Reference one from a pipeline's [`instructions`](/pipelines/config#instructions) as `{{lists.<name>}}`; it expands to the items joined by newlines. Lists are operator config, so the substituted text is trusted — not wrapped in `<untrusted>`. Referencing a name with no matching list is an error. Available on all triggers except MCP (which delivers a final prompt and skips template substitution entirely).

***

## Lifecycle

The request lifecycle has three operator-hook phases — `validate` → built-in `retrieve` → `augment` → spawn agent → `observe`. Hooks shell out to an external command and share a JSON state contract on stdin/stdout.

```yaml
lifecycle:
  validate:
    - type: command
      name: schema-check
      run: /usr/local/bin/bento-validate
      timeout: 10s
      match:
        equals: reviewer
  observe:
    - type: command
      name: notify-ops
      run: /usr/local/bin/notify-ops
      timeout: 5s
```

The built-in `retrieve` phase runs for pipelines with a retrieving [method](/knowledge-base/modes) (`bm25`/`vector`); its engine settings live under [`knowledge.retrieval`](#knowledge), not here.

### `validate`, `augment`, `observe`

Each is a list of `command` hooks. A `validate` failure vetoes the run; `augment` mutates state into the spawn; `observe` runs after spawn and cannot veto.

| Field | Description |
|-------|-------------|
| `type` | Must be `command`. (`prompt` and `http` are reserved in the schema but not yet implemented; configuring them errors the step.) |
| `name` | Step name; surfaces in trace output. |
| `run` | Absolute path to the executable. Relative paths are rejected at startup. |
| `timeout` | Optional duration string (`5s`, `30s`, `2m`). |
| `match.equals` | Optional — only run when the agent or skill name equals this value. |

***

## Sandbox

```yaml
sandboxes:
  backend: docker
```

Override the sandbox backend: `docker`, `podman`, or `daytona`. Omit this block to let the daemon auto-detect (`podman`, then `docker`). See [Pipeline Config](/pipelines/config#sandbox) to override per-pipeline.

Environment injection is per-pipeline (BIP-20): each pipeline lists the variables its runs receive in its own [`env:`](/pipelines/config#env) manifest.

`backend: daytona` requires a `daytona:` block alongside it (the API key itself comes from the `DAYTONA_API_KEY` environment variable):

```yaml
sandboxes:
  backend: daytona
  daytona:
    target: us            # optional region / target identifier
    apiUrl: https://...   # optional API base URL override
    organizationId: org_x # optional, for multi-org accounts
    image: ./Dockerfile.agent
    snapshot: bento-agent-default # optional, pre-built snapshot name
```

| Field | Description |
|-------|-------------|
| `target` | Region / target identifier passed to the Daytona client. |
| `apiUrl` | Override the Daytona API base URL. |
| `organizationId` | Organisation scoping for multi-org accounts. |
| `image` | Registry image reference Daytona can pull, or a path to a Dockerfile — a file path makes Daytona build and cache a snapshot from it. Unset → the run's resolved image is passed through as-is. |
| `snapshot` | Name of a pre-built Daytona snapshot. When set, runs boot from it directly and `image` is ignored at runtime — building a multi-GB image per run times out. |

Publish the snapshot once, before pointing runs at it (rebuild after any Dockerfile change):

```bash
DAYTONA_API_KEY=... pnpm --filter @bento/sandboxes publish-daytona-snapshot
```

The script builds `packages/images/agent-default/Dockerfile` into a snapshot named `bento-agent-default`; override with `--dockerfile` and `--name`. Re-publishing deletes and recreates the snapshot.

***

## Knowledge

```yaml
knowledge:
  retrieval:
    topK: 20
    minScore: 0.2
    rewrite:
      target: claude-high
      timeoutMs: 4000
```

Retrieval engine tuning — how qmd results are produced for retrieving pipelines (`method: bm25`/`vector`), `bento ask`, and `ask_knowledge`. See [Knowledge Base config](/knowledge-base/config) for the field reference. How results reach a run is the per-pipeline [`knowledge:` block](/pipelines/config#knowledge) and [`defaults.knowledge`](#defaults).

***

## Tunnel

```yaml
tunnel:
  provider: cloudflare
  mode: quick
```

Start a public tunnel at daemon startup. See [Public Access](/public-access) for setup.

***

## Logging

```yaml
logging:
  level: info
```

Log verbosity: `error`, `warn`, `info`, `debug`.

***

## Observability

```yaml
observability:
  langfuse:
    baseUrl: https://cloud.langfuse.com
    publicKey: pk-lf-...
    secretKey: ${LANGFUSE_SECRET_KEY}
  axiom:
    dataset: bento
    token: ${AXIOM_TOKEN}
  sentry:
    dsn: ${SENTRY_DSN}
    environment: production
```

Tracing for spawned agent CLIs (post-hoc), plus structured log shipping.

**Spawned agent CLIs (Langfuse):** after each agent run the daemon projects the persisted lineage, artifact manifest, and transcript into typed Langfuse observations (agent, span, generation, tool) via the Langfuse v5 OTel SDK — works for all backends and runtimes (claude, codex, Daytona). Each trace carries `trigger_id` in its metadata for cross-reference. The in-process orchestrator `query()` loop is not traced.

**Daemon logs (Axiom):** every log line the daemon emits (at the configured `logging.level`) is also shipped to Axiom as a structured OTel log record over OTLP — message as body, log data as attributes, and bento IDs (`run_…`, `trg_…`) lifted into `bento.*` attributes for per-run querying. Console output is unchanged. Records batch; the daemon drains the last batch on shutdown.

**Error/warn lines (Sentry):** every `error` and `warn` log line is forwarded to Sentry's HTTP envelope endpoint — message, level, and component only, never the log line's `data` bag. Best-effort: a failed send is warned once and dropped.

Omitting the block disables tracing; all sinks may be active at once.

| Field | Description |
|-------|-------------|
| `langfuse.baseUrl` | Langfuse instance base URL. |
| `langfuse.publicKey` | Project public key (`pk-lf-…`). |
| `langfuse.secretKey` | Project secret key (`sk-lf-…`). |
| `axiom.dataset` | Axiom dataset the logs land in. |
| `axiom.token` | Axiom API token (`xaat-…`). |
| `axiom.url` | API base URL override; defaults to `https://api.axiom.co`. |
| `sentry.dsn` | **Required.** Project DSN (`https://<key>@<host>/<project>`). |
| `sentry.environment` | Environment tag attached to every event (e.g. `production`). |

***

## Channels

```yaml
channels:
  slack:
    bots:
      standup:
        token: ${SLACK_STANDUP_TOKEN}
      reviewer:
        token: ${SLACK_REVIEWER_TOKEN}
  telegram:
    default_agent: reviewer
```

Named messaging identities a pipeline can post as. `channels.slack.bots.<name>` is a Slack app identity (one bot token); a pipeline names which bot to use in `output.slack.bot` — see [Pipeline output](/pipelines/config#output).

***

## Resources

```yaml
resources:
  wiki: docs/wiki
  blueprints: ["docs/blueprints/**/*.md", "external/specs/**/*.md"]
  recipes: docs/recipes
  agents: ".bento/agents"
  skills: ".bento/skills"
```

Override where the daemon discovers each resource type. Each value is a glob (or list of globs) relative to the project root, or a bare directory that expands against the type's default marker (`.md` for knowledge, `PERSONA.md`/`SKILL.md` for definitions). Omit a key to keep its default:

| Type | Default glob |
|------|--------------|
| `wiki` | `wiki/**/*.md` |
| `blueprints` | `blueprints/**/*.md` |
| `recipes` | `recipes/**/*.md` |
| `agents` | `agents/*/PERSONA.md` |
| `skills` | `skills/*/SKILL.md` |

`knowledge.sources.{wiki,blueprints,recipes}` is a legacy alias for the three knowledge keys; `resources` wins when both are set.

***

## Auth

```yaml
auth:
  tokens:
    - id: cli-prod
      secret: ${BENTO_TOKEN_CLI}
      scopes: ["pipelines:*", "agents:*"]
```

Static bearer tokens for authenticated access to the daemon, an alternative to CLI-issued tokens (`bento token issue`). Setting any token activates auth — every request must then carry `Authorization: Bearer <secret>`. See [Authentication](/authentication) for full details and remote topology.
