diffwarden keeps the public CLI small and routes SDK/provider detail through
configuration.
Project config is discovered as diffwarden.config.json from the current directory upward
to the Git repo root.
User config is discovered at:
$XDG_CONFIG_HOME/diffwarden/diffwarden.config.json~/.config/diffwarden/diffwarden.config.jsonwhenXDG_CONFIG_HOMEis unset
Create a starter user config with:
diffwarden init # in a TTY: guided discovery; non-TTY or --json: static starter
diffwarden init --discover # force the discovery scaffold (prompts in a TTY)
diffwarden init --json # static starter config, never interactiveIn a terminal, a bare diffwarden init drops into the guided discover → scaffold → confirm
flow. Pass --json, or run in a non-TTY (CI, piped), to write the static starter template
instead. init and reviewers add always write to the user config path above, never to a
project diffwarden.config.json. See Discovery & Setup.
The user config can be split into a fleet-portable base and a host-owned overlay:
…/diffwarden/diffwarden.config.json— the base, safe to sync across machines (dotfiles)…/diffwarden/diffwarden.config.local.json— the host-local overlay, never synced
At load time the overlay's raw JSON is merged over the base before validation; the merged result is what runs. With no overlay file present, behavior is byte-identical to a single config file — users who keep one diffwarden file see nothing new.
Precedence in one sentence: a project config wins wholesale (the overlay never applies to
it); otherwise the user base config is loaded and, if diffwarden.config.local.json sits
next to it, it is merged on top — local wins.
Merge rules:
| Value | Rule |
|---|---|
Objects (reporting, sdkOptions, …) |
Deep-merge key-wise; local wins per key |
Scalars / null / type mismatches |
Local value wholesale (null is a value, not a deletion) |
Arrays (modelCatalog, set members) |
Local array replaces wholesale |
reviewers |
Merged by id: a matching id overlays that entry's fields; a new id appends (and must be a complete entry, engine included) |
reviewerSets |
The map merges per set name; a local set replaces that base set's membership wholesale |
There is no per-host deletion: a base reviewer cannot be removed by the overlay — disable it
with enabled: false instead. The base must always remain a complete, valid, standalone
config (that is exactly what dotfiles syncing depends on); diffwarden doctor flags a base
that only validates with the overlay's help.
What belongs where: the reviewer catalog, reviewer sets, defaultReviewerSet, readonly,
and reporting are fleet policy — keep them in the base. Machine identity
(sdkOptions.machineId for droid) and per-host enabled toggles are host-owned — keep them
in the overlay.
The declarative reviewer commands take --local to target the overlay:
diffwarden reviewers add droid --transport cli --local
diffwarden reviewers edit droid --disabled --local # host-only toggle
diffwarden reviewers remove droid --local # clears this host's overridesIn the overlay, --enabled writes an explicit enabled: true (absence means "inherit
base", so re-enabling must be explicit to beat a base enabled: false). Per-field override
clearing is a hand edit; remove <id> --local clears a reviewer's whole overlay entry.
Reviewer sets are base-owned: there is no --local for reviewers set, and
add --local --set <name> is rejected. Base set membership must reference base reviewers —
an id that exists only in this host's overlay is rejected, since the synced set would break
on every other host (define a host-only set in the overlay file instead). The interactive
reviewers add, edit, and remove flows ask base-vs-local when an overlay file exists.
Base writes stay safe under an overlay: any base edit whose merged result would be invalid is refused naming both files, and a base write that an overlay key shadows prints a note that the effective value on this host did not change. A base config that is a symlink (the dotfiles layout) is updated through the link, never replaced by a regular file.
A worked fleet recipe:
-
Keep the base in dotfiles, with droid's entry shipping WITHOUT
sdkOptions.machineId. -
Per host, hand-write the overlay (no CLI verb sets
sdkOptionstoday):{ "reviewers": [ { "id": "droid", "sdkOptions": { "machineId": "<host-id>" } } ] } -
Flip per-host toggles with
diffwarden reviewers edit <id> --disabled --local. -
Add
diffwarden.config.local.jsonto the dotfiles ignore list. -
diffwarden doctoranddiffwarden reviewers listshow the overlay path and which values are host-local; identical base file hashes across hosts are your drift detector.
Rollback is deleting the overlay file. An overlay without a base config is ignored (an
overlay only overlays; doctor points this out), and an older diffwarden binary simply
ignores the overlay file. On a host where the base was synced away from under an overlay
entry, the next load fails loudly naming the orphan entry and the fix; diffwarden doctor
diagnoses which file is at fault.
Use explicit reviewers:
diffwarden review --target base:main --reviewer cursor
diffwarden review --target base:main --reviewer claude
diffwarden review --target base:main --reviewer pi:openrouter-high
diffwarden review --target base:main --reviewer cursor --reviewer pi:openrouter-highUse a configured reviewer set:
diffwarden review --target base:main --reviewer-set 2When no --reviewer or --reviewer-set is provided, config must define
defaultReviewerSet.
List configured reviewers and reviewer sets without invoking adapters or running preflight:
diffwarden reviewers list
diffwarden reviewers list --jsonConfigured reviewers must use engine for the reviewer family (claude, pi, codex,
etc.). Use transport: "sdk" for the SDK-backed path when you want to be explicit, or
transport: "cli" for executable-backed runs. Codex also supports a transport: "app-server"
path for ephemeral codex app-server reviews.
Three commands answer three different questions. None of them publish review comments, and discovery never spends model budget:
diffwarden reviewers discover— What could this host run? Probes every built-in reviewer engine for an installed executable or resolvable SDK package and for token-free auth signals (relevant environment variables and credential files), then classifies each candidate. It reads nothing from your config and writes nothing.diffwarden reviewers list— What is configured? Lists the reviewers, reviewer sets, anddefaultReviewerSetalready in config. It does not probe the host.diffwarden doctor— Will a configured reviewer actually run? Resolves reviewers from config and runs full adapter preflight (which may spawn CLIs or call provider APIs).
Discovery is shallow by default: it inspects PATH, resolvable SDK packages, environment
variables, and credential-file presence only. Pass --deep to additionally run adapter
preflight for present engines, which can spawn CLIs or make provider calls:
diffwarden reviewers discover
diffwarden reviewers discover --deep
diffwarden reviewers discover --jsonEach candidate is classified as one of available, missing_executable, missing_auth,
requires_env, unsupported_host, or preflight_failed. available candidates carry a
recommended config entry. JSON output uses schema_version: 1; it reports environment
variable names and credential-file paths that were probed but never secret values.
Write a discovered reviewer into the user config with reviewers add:
diffwarden reviewers add codex # add with defaults, id "codex"
diffwarden reviewers add claude --transport cli # add CLI-transport Claude
diffwarden reviewers add pi --id pi-fast --set 1 # custom id, also append to set "1"
diffwarden reviewers add grok --disabled # write a disabled placeholderreviewers add merges by id (re-adding an existing id updates it in place), appends to a
named reviewer set with --set without ever changing defaultReviewerSet, preserves every
other key in the file, and writes atomically. Adding a reviewer never enables it into the
default set silently; choose the set explicitly. Scaffold a whole config from discovery in one
step with init --discover, which writes the ready-to-use reviewers, a defaultReviewerSet,
and readonly: true, and refuses to overwrite an existing config.
Edit, remove, and manage set membership of existing reviewers with the same atomic, user-config-only write contract:
diffwarden reviewers edit codex --model gpt-5.1-codex # patch one field, keep the rest
diffwarden reviewers edit grok --enabled # clear a disabled placeholder
diffwarden reviewers set add 1 codex # add a configured id to set "1"
diffwarden reviewers set remove 1 codex # remove it from set "1"
diffwarden reviewers remove codex # delete it and prune it from all setsedit patches only the named fields (preserving sdkOptions, profile, enabled: false, and
every untouched key) and rejects overrides the resolved transport cannot honor before writing,
just like add. remove deletes the reviewer and prunes its id from every reviewer set so sets
never reference a missing reviewer. Both remove and set remove refuse to leave the set named
by defaultReviewerSet empty unless you pass --force; removing or editing an unknown id exits
non-zero and writes nothing. set add requires the id to be a configured reviewer.
Interactive setup is the default in a TTY. A bare diffwarden reviewers add opens an arrow-key
multiselect of discovered reviewers that are not already configured, then steps each selection
through a field editor for transport, model, effort, and the reviewer id. A bare diffwarden init
runs the discover/scaffold flow. In the model field, engines whose transport declares a live model
catalog — claude (SDK transport), cursor, codex, pi, opencode, copilot, and droid — replace free
text with an autocomplete picker fetched once per setup session behind a spinner; typing filters
the catalog, an off-catalog id commits in one Enter via the picker's creatable row, and "custom…"
keeps the full free-text escape hatch. A failed fetch (no auth, offline, timeout) degrades to free
text with a one-line notice naming the engine and its login command, and logging in then
re-entering the field recovers without restarting. When the fetched catalog lists a model's
supported effort levels, the effort menu narrows to exactly the efforts diffwarden can deliver to
that model over the configured transport, plus default. Gemini, grok, and antigravity have no
listing surface and keep plain free text. A bare reviewers edit (or edit <id> with no field flags) opens
a field editor for an existing reviewer's transport, model, effort, and enabled state, and a bare
reviewers remove lets you pick a reviewer and confirm (default No). In any menu, Esc or Ctrl-C steps back one level and cancels at the top, while
a ✕ quit choice exits immediately. In the field editor, submitting a blank model clears the model
override back to the engine default and choosing default effort clears the effort override;
changing transport clears a now-inert model/effort. Because the interactive edit replaces the
editor-managed fields, clearing a field in the picker removes it from config, unlike the
declarative reviewers edit --field value set-only patch. Prompts render to stderr, so stdout
stays machine-clean. Naming a target — an engine, an id — passing a field flag to edit, passing
--json, or running outside a TTY (CI, piped) stays fully declarative and never prompts; a
no-target setup command in a non-TTY exits with a usage error rather than hanging. --interactive
forces the guided flow — the discovered picker for a bare add, or the discover/scaffold flow for
init — and still requires a real TTY. It applies only to the no-engine add: combining
--interactive with a named engine is rejected with a usage error, since the two express opposite
intent (naming an engine is the declarative path).
Set enabled: false on a configured reviewer when it should stay in config but must not run,
for example when a local app, CLI, or provider is temporarily unavailable. Omitted enabled
means enabled.
{
"reviewers": [
{
"id": "droid-cli",
"engine": "droid",
"transport": "cli",
"enabled": false
}
]
}Disabled reviewers remain visible in diffwarden reviewers list and JSON output as
enabled: false. Selecting a disabled configured reviewer fails with a config error, whether
it is selected directly by id, through an engine:profile spec, through reviewerSets, or
through defaultReviewerSet. Diffwarden does not silently skip disabled reviewers because
that could change who reviewed a patch. Bare built-in specs such as claude, pi, or
droid are unaffected, even if a configured reviewer id happens to match a built-in name.
CLI flags take precedence over environment defaults.
DIFFWARDEN_REVIEWERS=cursor,claude,pi:openrouter-high
DIFFWARDEN_REVIEWER_SET=2
DIFFWARDEN_MODEL=anthropic/claude-sonnet-4-5
DIFFWARDEN_FALLBACK_MODEL=sonnet
DIFFWARDEN_EFFORT=high
# Optional: only set this when a review should have a wall-clock cap.
DIFFWARDEN_TIMEOUT_SECONDS=1800Reviewer selector environment defaults are only applied after a config file is discovered.
Without config, pass --reviewer or --reviewer-set explicitly.
Focused diff-backed reviews include the normal overview lane by default when one or more
--focus flags are present. Configure the default with reviewPlan.includeOverview:
{
"reviewPlan": {
"includeOverview": false
}
}CLI flags override config for one run:
diffwarden review --target base:main --reviewer-set 2 --focus "focus on state" --overview
diffwarden review --target base:main --reviewer-set 2 --focus "focus on state" --no-overview--overview and --no-overview only apply when at least one focus lane is requested.
Focus lanes are diff-backed instruction layers over uncommitted, base:<branch>, and
commit:<sha> targets; they are not compatible with repository-scoped custom:<text>.
Review history reports are off by default because they can contain finding bodies, reviewer explanations, file paths, and other source-adjacent review content.
Enable reports for one run:
diffwarden review --target base:main --reviewer-set 2 --reportUse repo-local history:
diffwarden review --target base:main --reviewer-set 2 --report --report-scope repoUse a custom directory:
diffwarden review --target base:main --reviewer-set 2 --report --report-dir ./reportsConfig can opt in globally or per project:
{
"reporting": {
"enabled": true,
"scope": "global",
"mode": "full"
}
}Reporting options:
enabled: write report history when true.scope:globalorrepo. Global writes under the user state directory. Repo writes under.diffwarden/reports/in the Git repo root.dir: custom report directory. Relative paths resolve from the CLI cwd and overridescope.mode:fullstores the full review artifact.metadataomits the full artifact and finding bodies while preserving titles, locations, priorities, confidence, reviewer metadata, and counts.
CLI flags take precedence over config, including --no-report for disabling a configured
report on one run. --out writes one explicit review artifact file; --report writes
date-partitioned history records for later external analysis.
Report records use report_schema_version: 3. Every report includes a provenance block
intended to make a run reproducible without persisting more patch content than necessary:
diffwarden.version: the Diffwarden CLI version that wrote the report.invocation: the requested target, reviewers or reviewer set, model, effort, timeout, strict mode, finding gate, and output mode when those options were supplied.config: the loaded config path and SHA-256 of the config file contents when a config file was used. When a host-local overlay was merged over the user config,config.localcarries the overlay's path and SHA-256 as a second{path, sha256}record;config.pathandconfig.sha256keep meaning the base (or project) file's bytes. The report does not embed config contents. Consumers that ignore unknown keys are unaffected.reviewer_selection: the requested reviewer specs or reviewer set plus the resolved reviewer ids that actually ran.target: for diff-backed targets, SHA-256 and byte count of the reviewed patch. Reports do not persist the patch text as provenance, sopatch_persistedis currently alwaysfalse.
Focus batch reports also record focus, include_overview, and the resolved review_plan
under invocation provenance. These fields are persisted in metadata mode because they are
needed to reproduce the review plan. The shared diff hash and byte count are stored once for
the batch target, not once per lane.
Reviewer entries preserve adapter output metadata, preflight metadata, and adapter usage data
when the adapter provides them. Adapter and executable version data are therefore best-effort:
SDK adapters record SDK/version details where they already discover them, while CLI adapters do
not run extra --version probes solely for reporting.
Executable-backed adapters include the resolved executable path plus requestedExecutable
and executableSource. executableSource: "config" means the executable string came from
reviewer executable config, usually cliOptions.executable and for Droid SDK
sdkOptions.executable; adapter-default means Diffwarden used the built-in executable name
and resolved it from PATH. These fields describe Diffwarden's launcher selection, not a
provider-observed runtime value.
Adapter metadata may include requestedModel, resolvedModel, modelResolutionSource,
requestedEffort, resolvedEffort, and effortResolutionSource. These fields preserve the
requested config/CLI value separately from the value Diffwarden can prove was selected.
config resolution sources mean the value came from Diffwarden reviewer configuration, which
is useful provenance but lower confidence than provider-observed runtime metadata. env means
the value came from DIFFWARDEN_MODEL or DIFFWARDEN_EFFORT. SDK adapters prefer
provider-observed values where available. CLI adapters report deterministic values that
Diffwarden passes on the command line, but provider-observed values from stable runtime
JSON/JSONL metadata take precedence when available. Claude CLI's single-model modelUsage
result is used only when no explicit runtime model field is present, and is normalized to remove
display-only formatting such as a trailing context-window suffix. Droid CLI stdout currently
omits model and effort fields, but Diffwarden can read Droid's local session settings file from
the returned session_id and report those values as provider-local metadata when the file is
available and Diffwarden did not already select model or effort from config, env, or per-run
overrides. Droid lookup starts with the encoded cwd directory and falls back to a one-level
session-id search under ~/.factory/sessions for path-encoding compatibility. When explicit
Droid CLI model or non-off effort settings are present, Droid session values are retained as
droidSessionModel and droidSessionEffort but are not promoted over the selected values.
Because Droid CLI omits an off effort flag, session settings may still provide the resolved
runtime effort for effort: "off" while preserving requestedEffort: "off". Pi CLI's assistant
message.model records are treated as runtime-result evidence, not startup configuration proof.
CLI adapters omit default model resolution when the executable does not expose a stable
machine-readable runtime value. Gemini remains supported for enterprise and paid API-key users
after Google's June 18, 2026 consumer/free Gemini CLI transition to Antigravity CLI, but no new
metadata-specific behavior is being added for it.
Droid CLI stream modes are not used for review metadata today. CLI reviews keep the single-shot
JSON/text contract plus session_id session-settings lookup for Factory UI-friendly routine
runs.
Reports also promote these fields into each reviewer summary as model_resolution and
effort_resolution objects. Run adapter metadata is preferred; preflight metadata is used only
when run metadata is unavailable.
Privacy mode affects review content, not run provenance. full reports include the full
review artifact, which can contain source-adjacent review text. For focus runs this is the
full ReviewBatchArtifact. metadata reports omit the artifact and finding bodies while
retaining titles, locations, priorities, confidence scores, counts, lane summaries,
provenance, usage data, and adapter/preflight metadata.
Model selection is a base CLI option via --model.
Effort, provider, and SDK-specific settings are represented as reviewer config. Simple
single-reviewer runs can use flags like --model and --effort; multi-reviewer/provider
runs should use named profiles.
Public effort values:
offminimallowmediumhighxhighmax
When flags, environment variables, and reviewer config all leave effort unset, Diffwarden
applies a per-transport default effort from the adapter capability matrix, reported with
effortSource: "diffwarden-default". Claude reviewers (SDK and CLI transports) default to
high; other engines currently have no default, so effort stays unset and the engine
decides. A reviewer effortCatalog that omits the default suppresses it. On the Claude SDK
transport, if model preflight proves the selected model does not support effort, the default
is silently dropped (recorded as effortDropped: "model-unsupported") instead of failing
the run. The Claude CLI transport has no model catalog access, so it passes --effort high
through and leaves unsupported-level resolution to the platform, matching how it already
handles explicit efforts.
Claude reviewers support a fallback model plus optional run limits. Other engines reject
these fields at config load time, and maxTurns is rejected for the Claude CLI transport
because the Claude CLI exposes no --max-turns flag.
{
"reviewers": [
{
"id": "claude-opus",
"engine": "claude",
"model": "opus",
"fallbackModel": "sonnet",
"maxTurns": 60,
"maxBudgetUsd": 2.5
}
]
}fallbackModel— model to retry with when the primary model is overloaded. Single-reviewer runs can override it with--fallback-model <id>orDIFFWARDEN_FALLBACK_MODEL. When the primary model is not a Sonnet-family model and no fallback is configured, Diffwarden defaults to Sonnet (reported asfallbackModelSource: "diffwarden-default"); Sonnet primaries get no default. On the SDK transport, preflight validates the fallback against the model catalog and reportsfallbackModelUsedafter the run when usage data can prove whether the fallback served it. On the CLI transport the flag is probed against--help: an explicit fallback on an older CLI is dropped withfallbackModelDropped: "cli-unsupported"metadata, while the diffwarden default is silently skipped.maxTurns— hard cap on agentic turns (SDK transport only).maxBudgetUsd— hard spend cap for the run. Exhausting it fails the reviewer with a budget-specific error instead of returning a partial review. A Claude CLI executable that lacks--max-budget-usdfails the run rather than silently dropping a spend cap.
Note: the config schema is strict, so older diffwarden versions that predate these fields
reject a shared user config (~/.config/diffwarden) that uses them. Keep every diffwarden
install that reads a shared config at least as new as the newest field you configure.
Provider-backed Pi profiles can keep provider-specific auth and base URL wiring in config
while leaving the public CLI surface unchanged.
Use named profiles for reusable reviewer sets and pin provider, model, and usually
effort. Bare --reviewer pi remains useful for ad hoc local runs, but it selects the first
authenticated Pi model when no model is configured; that choice can drift as auth state or
Pi's model registry changes.
{
"reviewers": [
{
"id": "pi-openrouter-high",
"engine": "pi",
"profile": "openrouter-high",
"provider": "openrouter",
"model": "anthropic/claude-sonnet",
"effort": "high",
"providerOptions": {
"baseUrlEnv": "OPENROUTER_BASE_URL",
"apiKeyEnv": "OPENROUTER_API_KEY"
},
"sdkOptions": {
"providerProfile": "openrouter"
}
}
]
}By default the Pi SDK adapter uses isolated, in-memory auth and only sees provider
credentials supplied through environment variables. Set sdkOptions.authSource to
"shared" to reuse the Pi CLI's on-disk login (auth.json) instead, including OAuth
logins such as openai-codex. This reads the same credentials the pi CLI uses and
auto-refreshes OAuth tokens with file locking, without spawning the CLI.
{
"reviewers": [
{
"id": "pi-shared-codex-high",
"engine": "pi",
"provider": "openai-codex",
"model": "gpt-5.5",
"effort": "high",
"sdkOptions": {
"authSource": "shared"
}
}
]
}authSource accepts "isolated" (default) or "shared". With "shared" you may set an
optional sdkOptions.authPath to point at a non-default auth.json (a leading ~ is
expanded to the home directory); authPath is rejected unless authSource is "shared".
Environment-backed provider auth still composes on top of shared credentials.
Use isolated auth with providerOptions.apiKeyEnv/baseUrlEnv when the profile should be
fully described by Diffwarden config and environment variables, such as CI or provider API
key workflows. Use shared auth when the intended provider/model depends on a Pi CLI OAuth
login that cannot be represented as an API key. In both modes, reusable provider-heavy
profiles should pin the model instead of relying on Pi's first authenticated model.
Note: "shared" reads real credentials and writes to auth.json on disk: the file (and
its parent directory) is created empty if it does not yet exist, and the file is rewritten
when an expired OAuth token is refreshed (the same behavior as the Pi CLI). This is the one
way a review run touches state outside the repository; the default "isolated" mode never
reads or writes auth.json.
The Pi SDK adapter always supplies an isolated in-memory SettingsManager for review runs.
Diffwarden does not inherit global or project Pi settings.json files, and it does not add
tool-call, turn, step, retry, or default wall-clock caps around Pi. A configured reviewer
timeout, when set, is the authoritative run-level circuit breaker.
Diffwarden passes explicit Pi runtime defaults for review sessions:
{
"transport": "auto",
"steeringMode": "one-at-a-time",
"followUpMode": "one-at-a-time"
}Set sdkOptions.settings on a Pi reviewer when a run needs a different Pi SDK runtime
setting without inheriting settings files:
{
"reviewers": [
{
"id": "pi-websocket",
"engine": "pi",
"sdkOptions": {
"settings": {
"transport": "websocket",
"thinkingBudgets": {
"high": 12000
}
}
}
}
]
}Supported sdkOptions.settings fields are transport, steeringMode, followUpMode, and
thinkingBudgets; unsupported keys are rejected rather than ignored. thinkingBudgets
supports minimal, low, medium, and high. Preflight and output metadata report those
runtime settings plus the effective Pi-native retry/provider/compaction settings that Pi will
use for the session. Provider request timeout and retry values may appear as SDK defaults when
Pi leaves them unset. HTTP idle timeout is reported as an SDK default when the installed Pi
SDK exposes it; otherwise metadata marks it as unavailable rather than inferring a value from
upstream docs.
SDK-backed families can be configured to use a CLI transport.
Droid users should prefer the CLI profile for routine reviews when Factory UI session
history matters. Droid CLI uses droid exec --use-spec, keeps Droid's default read-only
autonomy by leaving mission/unsafe autonomy flags unset, uses an explicit read/spec-control
tool allowlist, verifies that allowlist with --list-tools, adds a Diffwarden log group ID,
and relies only on an explicitly configured reviewer timeout rather than tool-call, turn, step,
or retry caps.
Codex CLI disables Codex web search by default with
web_search = "disabled"; set cliOptions.webSearch to "enabled" or "inherit" when a
reviewer should use a different policy.
Copilot defaults to SDK transport. Set sdkOptions.baseDirectory to use a dedicated Copilot
home, or sdkOptions.executable to force a Copilot-named runtime binary or readable .js
runtime entry for the SDK path. Diffwarden launches .js runtime entries through Node. SDK
runs stage only auth keys from the source Copilot home into a temporary Copilot home with empty
MCP config.
SDK reviews fail closed if the source Copilot home, resolved SDK runtime, or GitHub CLI auth
directory is inside the reviewed repository.
Set transport: "cli" and cliOptions.executable to run the Copilot CLI directly.
CLI runs use a run-scoped HOME/COPILOT_HOME, isolated GH_CONFIG_DIR, and scoped Windows
AppData paths, copy Copilot auth plus GitHub CLI hosts.yml auth from GH_CONFIG_DIR,
XDG_CONFIG_HOME, home, or Windows AppData locations, use -p/--prompt for non-interactive
mode with a short instruction that points at a run-scoped prompt file, and add only that prompt
directory plus a run-scoped tool-output temp directory with --add-dir. User-level Copilot
MCP config is replaced by an empty staged file; only repo-local MCP configs are parsed for
explicit --disable-mcp-server flags. The resolved Copilot CLI executable must be outside the
reviewed workspace, including executables found through PATH.
OpenCode CLI receives prompts on stdin and uses a generated diffwarden-review-* agent in
low-tool mode by default. Diffwarden supplies the patch in the prompt, allows only read,
glob, and grep through both OPENCODE_CONFIG_CONTENT and OPENCODE_PERMISSION, denies all
other OpenCode tool permissions, and tells OpenCode not to run the patch provenance command.
Diffwarden does not inject an OpenCode step cap; only a configured reviewer timeout limits the
run. Set cliOptions.agent to select an existing primary OpenCode agent. If
cliOptions.agent is set, or if OPENCODE_CONFIG_CONTENT, OPENCODE_CONFIG, or
OPENCODE_CONFIG_DIR is already present in the effective environment passed to OpenCode,
Diffwarden does not inject the generated agent config.
{
"reviewers": [
{
"id": "claude-cli",
"engine": "claude",
"transport": "cli",
"model": "sonnet",
"effort": "high",
"cliOptions": {
"executable": "/Users/auro/.local/bin/claude"
}
},
{
"id": "droid-cli",
"engine": "droid",
"transport": "cli",
"cliOptions": {
"executable": "/Users/auro/.local/bin/droid"
}
},
{
"id": "copilot-sdk",
"engine": "copilot",
"transport": "sdk",
"sdkOptions": {
"baseDirectory": "/Users/auro/.copilot-diffwarden",
"executable": "/Users/auro/.local/bin/copilot"
}
},
{
"id": "droid-sdk-local-computer",
"engine": "droid",
"transport": "sdk",
"sdkOptions": {
"executable": "/Users/auro/.local/bin/droid",
"machineId": "YOUR_DROID_COMPUTER_ID"
}
}
]
}Codex can opt into an app-server transport. By default this path uses the
existing shared Codex home, connects to its app-server socket when one is already running,
and launches codex app-server --listen unix:// only when no socket is available. Reviews
still start ephemeral read-only threads and record execEnabled: true because command
execution remains available as Codex-native model tool behavior; approval escalations are
denied. Diffwarden sets Codex web_search to "disabled" by default.
{
"reviewers": [
{
"id": "codex-app-server",
"engine": "codex",
"transport": "app-server",
"cliOptions": {
"executable": "/opt/homebrew/bin/codex"
},
"appServerOptions": {
"webSearch": "disabled",
"reviewMode": "structured"
}
}
]
}appServerOptions.webSearch accepts:
disabled: set Codexweb_search = "disabled". This is the default.enabled: set Codexweb_search = "live"for the review.inherit: do not override Codex web search for the thread.
In stdio-isolated mode, inherit copies the source Codex home's top-level web_search
setting into the temporary CODEX_HOME when one is configured.
appServerOptions.reviewMode accepts:
structured: use Diffwarden's schema-constrainedturn/startflow. This is the default.native: use experimental Codexreview/startmode and return Codex's rendered review text. This mode is text-only for Diffwarden artifacts unless the text contains parseableReviewResultJSON.
In native mode, Codex disables web search inside the review task regardless of the parent
thread's web_search setting. Diffwarden reports webSearchMode: "disabled" and preserves
the requested parent-thread setting as requestedWebSearchMode when one was configured.
Configured effort overrides still apply; Diffwarden passes them through thread config as
model_reasoning_effort because review/start has no per-request effort field.
The shared Codex home resolves from appServerOptions.codexHome, then
DIFFWARDEN_CODEX_HOME, then DIFFWARDEN_CODEX_AUTH_HOME, then $CODEX_HOME, then
$HOME/.codex. Shared mode intentionally uses that Codex home's auth, config, plugins,
apps, and daemon state. Diffwarden still sets approval policy never, uses a read-only
sandbox policy with network disabled, and denies approval escalations.
appServerOptions.codexHome applies to shared socket modes. In stdio-isolated mode,
Diffwarden creates a temporary CODEX_HOME for the app-server process and sources auth and
model-provider config from DIFFWARDEN_CODEX_AUTH_HOME, then $CODEX_HOME, then
$HOME/.codex.
Use a stable alternate Codex home when you want a reusable server without sharing the primary Codex config:
{
"reviewers": [
{
"id": "codex-app-server",
"engine": "codex",
"transport": "app-server",
"appServerOptions": {
"mode": "auto",
"codexHome": "~/.codex-diffwarden"
}
}
]
}appServerOptions.mode accepts:
auto: attach to an existing socket and launch only if none exists.attach: attach only and fail if the socket is unavailable.launch: reuse an existing socket or launch the shared server.stdio-isolated: use a temporaryCODEX_HOMEand stdio app-server process for each review.
Run the configured Droid CLI profile through the normal CLI:
diffwarden review --target base:main --reviewer droid-cli --model claude-opus-4-7 --effort highClaude reviewers default to sdkOptions.authMode: "auto" for both SDK and CLI transports.
Auto mode prefers a logged-in Claude Code account over ANTHROPIC_API_KEY and removes
Anthropic API credentials from the child environment when Claude Code auth is selected. This
keeps reviewer sets from silently consuming API credits when a local Claude Code subscription
is available.
Setup tokens minted with claude setup-token and exported as CLAUDE_CODE_OAUTH_TOKEN are
part of the claude-code mode. The credential strip keeps the token in the child
environment, and on hosts without a claude executable the SDK transport still runs in
claude-code mode through the SDK's bundled CLI (authMethod: "oauth_token"). Note that the
token is not validated until the first request.
Use claude-code or api-key to make the choice explicit:
{
"reviewers": [
{
"id": "claude-subscription",
"engine": "claude",
"sdkOptions": {
"authMode": "claude-code"
}
},
{
"id": "claude-api-key",
"engine": "claude",
"sdkOptions": {
"authMode": "api-key"
}
}
]
}Claude SDK and CLI reviews use a small read-only review surface: Read, Grep, and Glob.
Current Claude documentation treats allowedTools as auto-approval rules, not as an
availability allowlist. Diffwarden therefore restricts SDK built-in tools with tools, mirrors
that list into allowedTools, uses permissionMode: "dontAsk" so anything outside the
approved read tools is denied instead of prompting, and sets strictMcpConfig: true with an
empty MCP server map. It also sends explicit deny rules for high risk surfaces such as Bash,
PowerShell, Monitor, Edit, Write, NotebookEdit, WebFetch, WebSearch, Agent,
Skill, and Workflow.
Claude CLI reviews mirror the same policy with --tools, --allowedTools,
--disallowedTools, and --permission-mode dontAsk. The CLI path also disables session
persistence, settings sources, ambient MCP config, slash commands, and Chrome integration for
the review invocation.
LS is not included. The current Claude tools reference no longer lists an LS built-in tool,
and Glob is sufficient for path discovery inside Diffwarden's restricted review surface.
Diffwarden does not add Claude tool-call, turn, step, or retry caps around normal reviews; the
configured reviewer timeout, when set, is the run-level circuit breaker. Claude-native limits
may still apply,
including model context limits, structured-output retry behavior, provider output limits, and
built-in tool result limits.
Diffwarden checks local Claude Code executables for the required review policy flags before
using them. In sdkOptions.authMode: "auto", an outdated local Claude Code install falls back
to ANTHROPIC_API_KEY when an API key is available. In forced claude-code mode or CLI
transport, unsupported executables fail preflight with a missing-requirement error.