Skip to content
PrismaLens Docs

Configuration

PrismaLens reads its settings from a prismalens.config.yaml file. Nothing is required to get started — every setting has a built-in default — but you’ll usually want one to point the CLI at your telemetry stack and pick a harness.

Settings are merged from five layers, each one overriding the last (objects are deep-merged; arrays are replaced wholesale):

  1. Built-in defaults — baked into the CLI, so every field has a sane value even with no config file at all.
  2. Global layer — an OS-specific config directory, e.g. on Linux/WSL ~/.config/prismalens/config.yaml (honors XDG_CONFIG_HOME if set). A good home for BYO-key values you want on every project.
  3. Project layer — the file passed via --config, or, if that flag is omitted, the nearest prismalens.config.yaml found by walking up from the current directory to the filesystem root.
  4. Project-local layerprismalens.config.local.yaml, sitting beside the project config. Meant to be gitignored — a per-checkout override that doesn’t leak into the shared project file.
  5. CLI flags--model, --harness, and similar flags on investigate win over every file layer.
Terminal window
prismalens init

Writes a commented prismalens.config.yaml into the current directory, with every value sourced live from the schema’s defaults — it can’t drift from what the CLI actually uses. If a config file already exists, init leaves it untouched and does nothing.

Once you have a config, run prismalens doctor to check that it and your environment are ready for an investigation.

Any string value in the YAML can reference an environment variable with ${VAR_NAME} syntax. It’s resolved after all layers are merged and before the config is validated:

telemetry:
apiUrl: ${PRISMALENS_API_URL}

If a referenced variable isn’t set, loading the config fails with an error naming the missing variable — there’s no silent fallback to an empty string.

prismalens.config.yaml
# Repository under investigation (owner/name). Auto-detected from the git
# remote when omitted.
# repo: owner/name
# Tier-2 harness backend the supervisor rents.
agent:
# Which agent harness to rent: deepagents | claude-code
# (codex is a reserved value — planned, not yet available)
default: claude-code
# BARE model id, e.g. "gpt-oss:120b" — the chosen harness applies its own
# provider prefix. Omit to let the harness pick its own default.
# model: gpt-oss:120b
# Permission posture (the one dial PrismaLens exposes; it's translated into
# each harness's native settings): read-only | supervised | auto | dangerous
# permissions:
# mode: read-only
# Isolation boundary the harness runs in: auto | process | srt | e2b
# - process: the always-on cooperative floor (no OS enforcement).
# - srt: an enforced OS-level sandbox boundary; fails loudly if unavailable.
# - auto (default): tries srt, but only after a self-check confirms its
# network path is actually alive; otherwise degrades to the process floor
# and says so — never a silent downgrade.
# - e2b: an enforced cloud sandbox (needs E2B_API_KEY); only used when
# explicitly requested, never chosen by `auto`.
sandbox: auto
# Best-effort resource caps on the harness run. Every field is optional and
# unset means "no cap" — nothing here is enforced unless you set it.
# limits:
# wall_clock_ms: 60000 # deadline, enforced by every sandbox mode
# memory_mb: 512 # best-effort; only applied when sandbox is srt
# cpu_cores: 2 # best-effort; only applied when sandbox is srt
# Maximum number of turns (tool calls) the harness can make before being halted.
# Unset means the harness uses its own default.
# max_turns: 40
# Tier-1 synthesis provider and model. Used to synthesize the final report.
# synth:
# provider: openai # overrides auto-selection
# model: gpt-5-mini # omit to let provider pick default
# base_url: https://... # required for 'custom' provider
# Read-only telemetry + app endpoints the harness may query. Point these at
# your stack; the defaults assume a local dev setup.
telemetry:
prometheusUrl: http://localhost:9090
alertmanagerUrl: http://localhost:9093
apiUrl: http://localhost:5000
# Read-only log-query system the harness may curl (Loki, Elasticsearch, ...).
# logs:
# kind: loki
# url: http://localhost:3100
# Service catalog. The alert's `service` label (else `namespace`/`job`)
# selects an entry; the matched service's repo/tier/dependencies ride along
# into the investigation so a single-alert run isn't context-free.
# services:
# checkout:
# tier: tier-1
# repo: acme/checkout
# depends_on: [payments, inventory]
# Where runs, events, and reports are stored on disk (a prismalens.db record
# store plus a runs/<runId>/ workspace dir per run).
workspace:
base_dir: ~/.prismalens
# Unattended intake for `pl listen` (token-authed Alertmanager webhooks). The
# token is REQUIRED to start listening — keep it out of the file via env
# interpolation. See the reference below and the Unattended alerts guide.
# listen:
# token: ${PRISMALENS_LISTEN_TOKEN} # required for `pl listen`
# port: 4181 # local intake port
# grouping_window_ms: 60000 # debounce firing alerts into one group
# max_pending: 8 # queue depth before intake 503s
# slack_webhook_url: ${PRISMALENS_SLACK_WEBHOOK_URL} # optional report delivery
# caps:
# max_concurrent: 2 # investigations running at once
# max_per_hour: 10 # investigations started per rolling hour
# max_turns: 40 # per-run turn ceiling (unset ⇒ harness default)
# Per-harness native passthrough — untyped, forwarded straight to the rented
# harness. For `deepagents` (the deepagents-acp binary, driven over ACP),
# `args` is appended verbatim to its CLI invocation.
# harnesses:
# deepagents:
# native:
# args: [--memory, ./AGENTS.md]
ValueMeaning
auto (default)Prefers the enforced srt boundary, but only after confirming its network path actually works — a known WSL networking mode can leave it looking healthy while silently blocking all traffic. Falls back to process when that check fails, and reports the fallback rather than hiding it.
processThe cooperative floor — no OS-level enforcement, just the harness’s own read-only behavior.
srtRequires the enforced sandbox; throws if it isn’t available rather than quietly falling back.
e2bAn enforced cloud sandbox. Requires E2B_API_KEY and must be selected explicitly — auto never reaches for it.

All three fields are optional, and an unset field means “no cap” — nothing is silently enforced. wall_clock_ms is enforced by every sandbox mode; memory_mb and cpu_cores only take effect under sandbox: srt.

KeyTypeEnforced by
wall_clock_msinteger, millisecondsevery sandbox mode
memory_mbinteger, megabytessrt only
cpu_coresnumber (fractional allowed)srt only

Settings for pl listen, the unattended Alertmanager webhook receiver. token is the only key listen can’t start without; everything else has a default. For the end-to-end setup — the Alertmanager side, grouping, and Slack — see Unattended alerts.

KeyTypeDefaultWhat it does
tokenstring— (required to start)Shared bearer token every webhook request must present. Set it via env interpolation (${PRISMALENS_LISTEN_TOKEN}), never as a literal. Unset ⇒ pl listen refuses to start.
portinteger4181Local intake port. Bound on 127.0.0.1 only. 0 picks an ephemeral port.
grouping_window_msinteger (1000–120000)60000Debounce window: firing alerts arriving within it are grouped into one investigation.
max_pendinginteger8Alerts waiting or running before intake replies 503 (an empty queue always admits a payload, so an oversized group can’t wedge permanently).
slack_webhook_urlURLSlack incoming-webhook for report delivery. Unset ⇒ delivery disabled. Set via env interpolation (${PRISMALENS_SLACK_WEBHOOK_URL}).
caps.max_concurrentinteger2Investigations allowed to run at the same time.
caps.max_per_hourinteger10Investigations started per rolling 60-minute window.
caps.max_turnsintegerPer-run turn ceiling handed to the default harness. Unset ⇒ the harness’s own default.

Over-cap groups aren’t dropped — they’re recorded as terminal suppressed runs (visible in pl status) with the cap that turned them away.

A service catalog keyed by service name (ADR-0015). When an alert fires, its service label — falling back to namespace then job — selects the matching entry, and that service’s repo/tier/dependencies ride into the investigation so a single-alert run isn’t context-free. pl investigate --service <name> selects an entry explicitly, overriding the alert’s label.

services:
checkout:
tier: tier-1
repo: acme/checkout
depends_on: [payments, inventory]
KeyTypeDefaultWhat it does
<name>map keyThe service name matched against the alert’s service/namespace/job label (or --service). No match ⇒ the run proceeds from the bare alert.
tierstringFree-form criticality label (e.g. tier-1) carried into the investigation context.
repostringowner/name slug or a local path the harness reads for this service.
depends_onstring[][]Direct dependency service names — a blast-radius seed handed to the investigation.

Per-harness native passthrough (ADR-0017), keyed by harness id. Each entry has a single native map, forwarded untyped to that harness — an escape hatch for harness-specific knobs PrismaLens doesn’t model. Which keys a harness actually consumes differs:

HarnessWhat native does
deepagentsnative.args is appended verbatim to the deepagents-acp CLI invocation. native.shellAllowList and native.sandbox are rejected with an error — the published deepagents-acp binary has no such flags; use the --sandbox port for real isolation instead.
claude-codenative is merged into the Claude Agent SDK query options for the run.
harnesses:
deepagents:
native:
args: [--memory, ./AGENTS.md]

The permission posture (agent.permissions.mode) and the isolation boundary (agent.sandbox) are not native knobs — they’re first-class dials PrismaLens translates itself. Don’t try to set them through native.

Every run persists to workspace.base_dir (~/.prismalens by default): a prismalens.db record store plus a runs/<runId>/ workspace directory per run. These grow unbounded — PrismaLens prunes nothing on its own, and the footprint climbs much faster under pl listen, where alerts investigate themselves around the clock.

There’s no built-in retention policy or prune command yet, so cleanup is manual:

Terminal window
# Drop old per-run workspace dirs (keeps the record store)
find ~/.prismalens/runs -maxdepth 1 -mtime +30 -type d -exec rm -rf {} +
# Full reset — removes all runs, events, and reports
rm -rf ~/.prismalens

Point workspace.base_dir at a volume with room to spare if you run pl listen long-term, and keep an eye on prismalens.db (it holds every run’s metadata, events, and report). Deleting ~/.prismalens wholesale is also the uninstall cleanup step — nothing lives outside it except your config file.