# Installer preflight: CLI version gate & environment checks

> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
> Last verified against commit `bdee731` (2026-08-05).
> Drift check: `git log --oneline bdee731..HEAD -- src/ClaudeDo.Worker/Lifecycle/ClaudeCliPreflight.cs src/ClaudeDo.Worker/ClaudeDo.Worker.csproj src/ClaudeDo.App/ClaudeDo.App.csproj .gitea/workflows/release.yml src/ClaudeDo.Installer/Checks src/ClaudeDo.Data/Environment/ExecutableResolver.cs`
> Stable structure only (no line numbers). See docs/explore-notes/README.md.

## Implementation status (as of 2026-08-05)

The research below (§1–5) led to an implementation, but it is **not on `main` yet** — it exists
on two unmerged task branches:

- `claudedo/06aca9b3afec4b939f59b627bfe21737` — `src/ClaudeDo.Installer/Checks/*`
  (`GitCheck`, `GitIdentityCheck`, `WriteAccessCheck`, `PortCheck`, `ClaudeCliCheck`,
  `ClaudeVersionCheck`, `ClaudeAuthCheck`, `PermissionModeAutoCheck`, `EnvironmentCheckService`,
  `ClaudeCliLookup`) plus `SystemCheckPage` (the Fresh-Install wizard page hosting them) and its
  own copy of `src/ClaudeDo.Data/Environment/ExecutableResolver.cs`.
- `claudedo/40272c0bb3b14562b59c022d09c382b6` — the original `ExecutableResolver.cs`, plus wiring
  it into `ClaudeDo.Worker`'s `ClaudeCliPreflight` and `ClaudeProcess` (the actual root-cause fix:
  both used to spawn `claude` with `UseShellExecute = false` and no `.cmd`/`.bat` shim resolution,
  so an npm-installed `claude.cmd` was invisible to the Worker even though it worked in a shell).

The two branches were authored independently and each vendored its own copy of
`ExecutableResolver.cs` (identical except `06aca9b3` adds a `FallbackDirectories()` diagnostic
helper); merging both cleanly requires picking one copy, not literally running `git merge` twice.
Two planned follow-up tasks — a "Claude Help Me" button and a Config-mode Diagnose section —
never got past a blocked first step, precisely because this prerequisite work wasn't on `main`
when they ran. See `Environment Checks` in `src/ClaudeDo.Installer/CLAUDE.md` and `docs/open.md`
for the current gap and the manual verification checklist.

What's confirmed as **matching the research below**: `ClaudeVersionCheck.MinimumVersion` is
`2.1.220`, exactly the "verified-floor, not a proven minimum" constant from §3. `ClaudeAuthCheck`
uses `claude auth status --json` exactly as recommended in §4, parsing only the `loggedIn` field.
`PermissionModeAutoCheck` is the static "is `auto` listed in `--help`" check recommended in §2 —
real org/model/plan eligibility is deliberately **not** checked, matching the recommendation not
to build that (a real task run surfaces a startup rejection fast enough on its own). No .NET
Desktop Runtime check was implemented as an `IEnvironmentCheck` (§5's registry-key detection
remains a documented-but-unbuilt option, not currently gating anything).

---

Pure research, no code changed. Answers the five questions from the "Root Cause
`--permission-mode auto`" task. Sources: the locally installed CLI (`claude --version` /
`--help`), the official docs at `code.claude.com` (fetched 2026-08-05), and this repo's own
csproj/workflow files.

## 1. Why can `--permission-mode auto` fail?

**It is very rarely a CLI-version problem.** Per the official permission-modes doc
(`code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode`), auto mode
requires **all** of:

- **Plan**: any plan (Free/Pro/Max/Team/Enterprise) qualifies in principle.
- **Organization**: on Team/Enterprise, on by default; an admin can disable it org-wide via
  `permissions.disableAutoMode: "disable"` in managed settings. When disabled this way, the CLI
  **"rejects `--permission-mode auto` at startup"** (verbatim from the doc) — not a runtime
  fallback, a hard reject.
- **Model**: on the Anthropic API / Claude Platform on AWS — Opus 4.6+, Sonnet 4.6+, or Fable 5.
  On Bedrock / Vertex / Foundry / signed-in gateway sessions — only Sonnet 5, Opus 4.7+, Fable 5.
  Older models (Sonnet 4.5, Opus 4.5, Haiku, claude-3-*) are **not supported on any provider**.
  A per-org `availableModels` restriction that only allows an old model would silently make
  auto mode unavailable even on a Team/Enterprise org that hasn't touched `disableAutoMode`.
- **Provider opt-in (historical)**: on Bedrock/Vertex/Foundry/gateway, CLI **v2.1.158–v2.1.206**
  required `CLAUDE_CODE_ENABLE_AUTO_MODE=1`; **v2.1.207** removed that requirement. This does
  **not** apply to a normal claude.ai / Console (Anthropic API) login — only to those four
  provider types.

A separate, easy-to-hit trap: `defaultMode: "auto"` in **project or local** settings
(`.claude/settings.json`, `.claude/settings.local.json`) is silently **ignored** since v2.1.142
— it must live in `~/.claude/settings.json` (user scope). A repo that ships `defaultMode: auto`
in its own `.claude/settings.json` will start in Manual mode with **no error at all**. This
doesn't apply to ClaudeDo's case (it passes `--permission-mode auto` as an explicit CLI flag,
not via a checked-in settings file), but is worth knowing if the failure mode was "silently
starts in Manual" rather than "CLI errors out".

Quoted from the docs, verbatim: *"If Claude Code reports auto mode as unavailable, one of these
requirements is unmet; this is not a transient outage."*

**Not verifiable**: which of the above actually hit the colleague — we have no diagnostic from
their machine (no `claude auth status --json` output, no `claude --version`, no org name). Do
not guess; if this recurs, capture `claude auth status --json` and `claude --version` from the
affected machine before further debugging.

## 2. How to reliably detect whether `auto` is supported

**Static, cheap, always safe (no API call):**
- `claude --version` — semver string, e.g. `2.1.220 (Claude Code)`.
- `claude --help` — the `--permission-mode <mode>` line lists the accepted enum. Confirmed by
  testing an invalid value locally:
  ```
  $ claude -p "test" --permission-mode bogus
  error: option '--permission-mode <mode>' argument 'bogus' is invalid. Allowed choices are
  acceptEdits, auto, bypassPermissions, manual, dontAsk, plan.
  ```
  This is validated by the CLI's arg parser (Commander.js) **before any network call** — exits
  1 immediately. So checking that `auto` is one of the listed choices is a legitimate, free,
  fast static check — but it only proves the *flag* is recognized, **not** that auto mode is
  actually usable (org/model/plan gating happens later, at session start, not at arg-parse time).
- `claude auth status --json` — cheap, local/fast, **does not send a prompt**. Returns:
  ```json
  { "loggedIn": true, "authMethod": "claude.ai", "apiProvider": "firstParty",
    "email": "...", "orgId": "...", "orgName": "...", "subscriptionType": "team" }
  ```
  This is the answer to question 4 (see below) and also gives `subscriptionType`/`orgName` —
  useful context but **still not a direct "is auto mode eligible" answer** (doesn't report the
  active model or `disableAutoMode` policy).

**No cheap subcommand exists for full eligibility.** Checked `claude auto-mode --help`: it only
has `config` (effective auto-mode *rule* config — allow/deny lists, not eligibility),
`defaults` (same, shipped defaults), `critique`, and `reset`. None report plan/model/org
eligibility. `claude doctor` (non-interactive) does **not** report auto-mode eligibility either
— it explicitly says *"For a full setup checkup that can also fix issues, run `/doctor` in a
session"*; the in-session `/doctor` slash command is the one the docs say proposes
`defaultMode: auto` when eligible, but that requires an interactive session, not a scriptable
preflight.

**Dynamic (real probe) is the only way to fully confirm eligibility**, and per the docs an
org-disabled or otherwise-ineligible account **rejects at startup** (fast, before any model
turn) — so a probe doesn't have to be a full expensive run. A minimal probe such as
`claude -p "ok" --permission-mode auto --max-turns 1 --output-format json` would fail fast on
ineligibility (reject at startup) but still costs one real turn + tokens on the success path,
and still requires a working prompt/response round trip on the happy path. **Recommendation**:
don't build this into an automated preflight; a static version+flag check plus `auth status`
covers the reliably-detectable ground, and a real first task run will surface an auto-mode
rejection immediately and cheaply (fails at startup, not mid-task) if it's actually unavailable.
**Implemented as:** `PermissionModeAutoCheck` (Warning) — the static flag-listed check only.

## 3. Minimum CLI version for the flags ClaudeDo uses

Flags used (from `ClaudeArgsBuilder` per `src/ClaudeDo.Worker/CLAUDE.md`): `--permission-mode
auto`, `--effort`, `--agents`, `--json-schema`, `--append-system-prompt`, `--output-format
stream-json --verbose`, `--resume`, and (installer) `claude mcp add --transport http --scope
user`.

**Not verifiable precisely.** All eight of these are foundational, long-established flags.
The official changelog (`raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md`)
only retains roughly the last ~40 entries (oldest visible: `2.1.181`); auto mode itself, and
`--json-schema`/`--resume`/`mcp add --transport` all clearly predate that window (the earliest
found reference is a *bug fix* mentioning "sessions created before v2.1.85" for `--resume`,
implying `--resume` existed well before 2.1.85). There is no accessible source that pins an
"introduced in vX" date for any of these eight flags — guessing one would violate the task's
explicit instruction not to invent a source-less root cause.

What **is** sourced, from the docs fetched 2026-08-05:
- `--json-schema` + invalid-schema handling: before v2.1.205, an invalid schema was silently
  ignored (returned unstructured text); v2.1.205 made it a hard `Error: --json-schema is not a
  valid JSON Schema` exit. Not a "does it exist" gate, but changes error-handling behavior
  ClaudeDo might currently rely on failing loudly.
- `--output-format stream-json --verbose` + `system/init.capabilities` array requires v2.1.205+
  (absent before). Not currently consumed by ClaudeDo per the Worker CLAUDE.md's stream handling
  description, so not a hard requirement today.
- `system/init.mcp_server_errors` field requires v2.1.219+. Not currently consumed by ClaudeDo.
- Manual-mode label/`manual` alias requires v2.1.200+ — irrelevant, ClaudeDo passes `auto`
  explicitly, never `manual`.
- Auto mode's Bedrock/Vertex/Foundry/gateway opt-in-env-var requirement was removed in v2.1.207
  — irrelevant for a direct claude.ai/Console login (this machine: `apiProvider: "firstParty"`).

**Decided constant** (see below) is therefore **the newest version we can positively confirm
works end-to-end on this machine** (`2.1.220`), not a proven theoretical minimum — because no
lower true minimum is derivable from available sources without guessing.
**Implemented as:** `ClaudeVersionCheck.MinimumVersion = new Version(2, 1, 220)` (Error).

## 4. Detecting "CLI is logged in" without sending a prompt

`claude auth status --json` (confirmed working, instant, no API/model call):
```json
{ "loggedIn": true, "authMethod": "claude.ai", "apiProvider": "firstParty",
  "email": "...", "orgId": "...", "orgName": "...", "subscriptionType": "team" }
```
This is the CLI's own maintained answer — cheaper and more robust than parsing
`.credentials.json` directly (format may be internal/undocumented and is a hard-blocked file
for this task's own tooling; the docs page confirms it lives at
`%USERPROFILE%\.claude\.credentials.json` on Windows but say nothing about its schema being a
stable public contract). `claude auth status --text` is available for a human-readable variant;
`--json` is the default and the right one for a preflight to parse.
**Implemented as:** `ClaudeAuthCheck` (Error) — parses only the `loggedIn` boolean; any other
field, or a non-zero exit code, or unparseable JSON, becomes `Unknown` rather than `Failed`.

## 5. .NET runtimes required by the published `app\` / `worker\` artifacts

From `.gitea/workflows/release.yml` (the only build/publish pipeline in this repo) and the two
csproj files:

- **`ClaudeDo.App`** (`net8.0`, Avalonia, `WinExe`) — published via
  `dotnet publish ... -r win-x64 --self-contained true`. **Self-contained**: bundles its own
  .NET 8 runtime. **No .NET runtime needs to be pre-installed** on the target machine for the
  app itself.
- **`ClaudeDo.Worker`** (`net8.0`, `Microsoft.NET.Sdk.Web`, ASP.NET Core) — same treatment:
  `-r win-x64 --self-contained true`. Also fully self-contained; no ASP.NET Core runtime needs
  to be pre-installed.
- **`ClaudeDo.Installer`** (`net8.0-windows`, WPF) is the one exception: published
  `--self-contained false -p:PublishSingleFile=true` — **framework-dependent**. The csproj
  comment explains why: *"the WPF runtime pack isn't distributed for cross-compile on Linux CI,
  which made self-contained bundles crash on startup with AV in the apphost."* The target
  machine **must** have the **.NET 8 Desktop Runtime (x64)** installed before running
  `ClaudeDo.Installer.exe` — this is the actual runtime-preflight gap, not the app/worker.

**How to check on a target machine**:
- `dotnet --list-runtimes` — look for a `Microsoft.WindowsDesktop.App 8.0.x` line (Desktop
  Runtime, required by the installer). Requires the `dotnet` CLI itself to be on PATH; not
  guaranteed present on a fresh machine that never installed the SDK, only the runtime — in
  that case `dotnet` may not exist at all even though the runtime DLLs do.
- Registry fallback (works even without the `dotnet` CLI on PATH):
  `HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App` —
  each installed version is a subkey/value here. This is the standard documented detection
  mechanism for .NET Desktop Runtime presence on Windows and is what most installer-detection
  tooling (e.g. Squirrel, WiX bundles) uses instead of shelling out to `dotnet`.
- **Not independently verified in this task**: the exact registry key shape wasn't inspected
  live (would require reading `HKLM\SOFTWARE\dotnet\...` on this machine, which is standard
  .NET installer-detection convention, but out of scope to screenshot/dump here since the task
  is docs-only and this is a well-documented, non-project-specific Windows convention).
  **Not implemented** as an `IEnvironmentCheck` — none of the shipped checks verify the Desktop
  Runtime; if the Installer itself is running at all, .NET 8 Desktop Runtime is implicitly
  present (framework-dependent publish would otherwise fail to launch).

## Beschlossene Konstanten

| Constant | Value | Confidence |
|---|---|---|
| Minimum CLI version | `2.1.220` | **Verified-floor, not a proven minimum.** This is the newest version confirmed installed and working end-to-end on a dev machine for every flag ClaudeDo uses. No lower true minimum could be sourced (see §3) — treat any lower value as a guess. |
| Credentials file path | `%USERPROFILE%\.claude\.credentials.json` (Windows) | Sourced from official docs; content/schema not inspected (hard-blocked secrets file). |
| Login-check command | `claude auth status --json` | Verified locally, instant, no model call. Fields: `loggedIn`, `authMethod`, `apiProvider`, `email`, `orgId`, `orgName`, `subscriptionType`. |
| App/Worker runtime requirement | **None** — self-contained win-x64 publish | Sourced from `.gitea/workflows/release.yml`. |
| Installer runtime requirement | **.NET 8 Desktop Runtime (x64)** must be pre-installed | Sourced from `.gitea/workflows/release.yml` comment + `ClaudeDo.Installer.csproj`. |

## Erkennungsstrategie pro Check

| Check | Type | Command | Expected pass output | Implemented as |
|---|---|---|---|---|
| git present + version | static | `git --version` (via `ExecutableResolver`) | resolves, exit 0 | `GitCheck` (Error) |
| git identity set | static | `git config --get user.name` / `user.email` | both non-empty | `GitIdentityCheck` (Warning) |
| install dir + data dir writable | static | probe-file write/delete | succeeds | `WriteAccessCheck` (Error) |
| SignalR/ExternalMcp ports free | static | `TcpListener` bind probe + owning-process lookup | free, or owned by running `ClaudeDo.Worker` | `PortCheck` (Warning) |
| CLI present + version | static | `claude --version` | `X.Y.Z (Claude Code)`; parse and compare `X.Y.Z >= 2.1.220` | `ClaudeCliCheck` (Error) / `ClaudeVersionCheck` (Error) |
| `auto` recognized as a flag value | static | `claude --help` (or trigger the parse error path) | `--permission-mode <mode>` help text lists `auto` among the choices | `PermissionModeAutoCheck` (Warning) |
| CLI logged in | static/cheap | `claude auth status --json` | exit 0, `loggedIn: true` | `ClaudeAuthCheck` (Error) |
| Auto mode actually eligible (org/model/plan) | **not statically detectable** | none exists | N/A — see §2; don't build this, let a real task run surface a fast startup rejection instead | not implemented (by design) |
| .NET Desktop Runtime present (installer only) | static | `dotnet --list-runtimes` (if `dotnet` on PATH) or registry `HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App` | a `Microsoft.WindowsDesktop.App 8.0.x` entry exists | not implemented (see §5) |
| App/Worker runtime present | **not needed** | — | self-contained, nothing to check | not implemented (not needed) |

## Not verifiable (explicit)

- The actual root cause on the colleague's machine — no diagnostic data was captured from it.
- Exact stderr/exit-code wording the CLI prints when auto mode is rejected at startup for an
  ineligible org/model (docs state the *behavior* — "rejects `--permission-mode auto` at
  startup" — but not the literal message; this machine's account is eligible, so it couldn't be
  reproduced locally).
- A true (not just "newest confirmed") minimum SemVer for `--effort`, `--agents`,
  `--json-schema`, `--append-system-prompt`, `--resume`, `claude mcp add --transport http
  --scope user` — all predate the retrievable changelog window.
- Live inspection of the `HKLM\SOFTWARE\dotnet\Setup\InstalledVersions` registry shape on this
  machine (documented Windows convention, not independently screenshotted here).
