Skip to content

Custom Providers ​

The Providers tab in the dashboard shows what ADHDev can detect and drive on a machine — it is not a place to build a new provider from scratch. To add support for a CLI, IDE, or agent ADHDev doesn't already know about, you write a small JSON file (a "provider manifest") and drop it into a folder the daemon already watches. This page walks through that flow end to end.

What a provider is ​

ADHDev has four provider categories:

  • cli — a terminal agent driven over a PTY (pseudo-terminal), like Claude Code or Codex CLI. The daemon types into the terminal and reads the screen back.
  • acp — an agent that speaks the Agent Client Protocol over stdio (structured messages, not screen-scraping).
  • ide — a CDP-driven code editor (Cursor, VS Code forks, etc.), inspected and controlled via Chrome DevTools Protocol.
  • extension — a CDP-driven IDE extension webview (Cline, Roo Code, etc.).

Realistically, cli is the category worth hand-writing. A CLI provider is a JSON manifest plus a JSON state-machine spec that describes how to recognize "generating" vs "idle" vs "waiting for approval" on screen — no code required.

ide and extension providers are a different story: they need real CDP automation scripts (JavaScript that runs against the IDE's DevTools connection to open panels, read chat, send messages, and so on). That's a meaningfully bigger lift than describing a terminal's screen states, and it's outside the scope of this page — see the internal Provider SDK guide references below if you want to go there anyway. acp providers are also workable by hand (no PTY screen-scraping needed) but are less common to add than a CLI.

The rest of this page is about CLI providers.

Where files go ​

The daemon loads providers from a small set of directories, in this precedence order (later wins over earlier for the same provider type):

  1. Upstream — <configDir>/providers/.upstream/ — the official, auto-synced bundle. Don't edit this by hand; it gets overwritten.
  2. External sources — <configDir>/external/<source-name>/ — 3rd-party git sources you register from the dashboard's Sources panel.
  3. User overrides — <configDir>/providers/ (everything except.upstream/) — your own custom or overriding manifests. This always wins. On a normal install <configDir> is ~/.adhdev, so this is ~/.adhdev/providers/.

Each provider lives at:

<root>/<category>/<type>/
  provider.v1.json      ← manifest (required)
  specs/
    <version>.json       ← FSM state-machine spec (required for cli)

Dropping a cli/<type>/provider.v1.json into ~/.adhdev/providers/ is enough — the daemon does not need it registered anywhere else.

If nothing shows up, check whether a providerDir is already set. An explicit providerDir (set via the Sources panel or config — see "Pointing the daemon at a different folder" below) replaces the default ~/.adhdev/providers/ as the user-overrides folder; it does not add to it. The daemon's own log records this as userDirSource: "explicit". If a machine already has one configured, your new manifest needs to go into that folder instead of ~/.adhdev/providers/, and hot-reload only ever watches the one active folder (see "How to tell which folder is active" below).

"Overrides upstream" means what it says ​

If you create a provider whose type matches one the daemon already ships (say, a tweaked claude-cli), your copy in ~/.adhdev/providers/cli/claude-cli/ is loaded instead of the upstream one. The daemon logs this explicitly (⚠ OVERRIDES upstream) so it's visible when it happens. This is also how you patch a single field of a built-in provider without waiting for an upstream fix — copy the whole provider directory into your user folder and edit it there.

Pointing the daemon at a different folder ​

By default the daemon only watches ~/.adhdev/providers/ for user overrides. If you want to keep your custom providers somewhere else (e.g. a git checkout you're actively editing), set an explicit provider directory:

  • Dashboard: Machine page → Providers tab → Advanced → set Provider source mode (normal or no-upstream) and Provider directory, then Apply & reload. no-upstream disables the auto-synced upstream bundle entirely and serves only your own directory plus whatever's in external/.
  • Config file: set providerDir (and optionally providerSourceMode) in the daemon's config.

Either way, applying the change calls the daemon's set_provider_source_config command, which reloads the provider map immediately — no daemon restart needed.

How to tell which folder is active ​

Since an explicit providerDir replaces the default rather than adding to it, it's worth confirming which folder the daemon is actually watching before you go looking for why a manifest isn't loading:

  • Dashboard: Machine page → Providers tab → Advanced shows the current Provider directory value (blank means the default, ~/.adhdev/providers/).
  • Daemon log: look for the line Hot-reload watcher active: <dir> — <dir> is the exact folder being watched. The daemon log lives at ~/.adhdev/logs/daemon-YYYY-MM-DD.log regardless of install type (cloud daemon, standalone, or preview instance).

Hot reload for the user-overrides folder ​

While the daemon is running, it watches your user overrides directory (~/.adhdev/providers/, or whatever you set providerDir to) for .js and .json file changes — and only that one folder; there's exactly one active user-overrides directory at a time, never both. Editing a manifest there gets picked up within about 300ms — no restart, no manual reload. This watch does not cover .upstream/ or the external/ sources tree; changes there need adhdev provider reload (or a dashboard refresh) to take effect.

Quick start: adhdev provider init ​

The fastest way to get the two files below on disk is to let the CLI generate them:

bash
adhdev provider init my-cli --dir ./my-cli

This scaffolds provider.v1.json + specs/1.0.json in the current v1 shape — the same shape this page walks through by hand next, with the binary name derived from <type> (strip a trailing -cli/-acp, or pass --binary explicitly) and placeholder regexes in the spec you still need to replace with patterns that actually match your target command's screen output. adhdev provider init my-acp --category acp --dir ./my-acp does the same for a declarative ACP provider (a single provider.v1.json, no spec file — see "What a provider is" above). init refuses --category ide and --category extension: those need hand-written CDP automation scripts specific to one IDE/webview, so there's no generic template that would produce something that actually runs.

Run adhdev provider validate ./my-cli right after — it will fail on the placeholder regexes (they never match), which is the point: it tells you exactly what's still a stub versus what the schema already accepts.

A minimal working CLI provider ​

CLI providers are routed through a declarative finite-state-machine engine (the "spec"). A spec is mandatory — the older script-based CLI engine was removed, and a CLI provider with no resolvable spec now fails to launch with an explicit error rather than falling back to something weaker. So a minimal provider is two files: the manifest, and the smallest valid spec. (This is the same shape adhdev provider init scaffolds above — read on for how each piece works if you're editing the generated files or writing them by hand.)

Choosing a target command ​

For a manifest you can actually test live, pick a real, harmless, already-installed interactive command. python3 -q is a good choice: it ships on virtually every macOS and Linux machine, starts in "quiet" mode (skips the version banner so there's less to match against), and its >>> prompt is a simple, stable marker for "idle" that doesn't depend on shell configuration the way a raw bash prompt (PS1) would.

provider.v1.json ​

json
{
  "$schema": "https://registry.adhf.dev/schemas/v1/cli/provider.schema.json",
  "type": "python-repl",
  "name": "Python REPL",
  "category": "cli",
  "binary": "python3",
  "spawn": {
    "command": "python3",
    "args": ["-q"],
    "shell": false
  },
  "compatibility": [
    { "ideVersion": ">=0.0.0", "spec": "specs/1.0.json" }
  ]
}

Only fields the schema actually requires (type, name, category, binary, spawn) plus compatibility, which is how the loader finds the spec file below.

specs/1.0.json ​

json
{
  "$schema": "adhdev:cli/spec@4",
  "id": "python-repl",
  "name": "Python REPL",
  "binary": "python3",
  "send_message": {
    "submit_key": "\r"
  },
  "states": [
    { "id": "starting", "label": "Starting", "initial": true, "status": "idle" },
    { "id": "idle", "label": "Ready", "status": "idle" },
    { "id": "busy", "label": "Evaluating", "status": "generating" }
  ],
  "transitions": [
    {
      "label": "startup → idle",
      "from": "starting",
      "to": "idle",
      "when": { "matches": ">>>\\s*(?=\\n|$)" }
    },
    {
      "label": "idle → busy",
      "from": "idle",
      "to": "busy",
      "min_hold_ms": 200,
      "when": { "not": { "matches": ">>>\\s*(?=\\n|$)" } }
    },
    {
      "label": "busy → idle",
      "from": "busy",
      "to": "idle",
      "min_hold_ms": 300,
      "when": {
        "all": [
          { "matches": ">>>\\s*(?=\\n|$)" },
          { "stable_ms": 500 }
        ]
      }
    }
  ]
}

This is the actual minimum the FSM validator enforces: id, binary, send_message.submit_key, a non-empty states[] with exactly oneinitial: true state, and a transitions[] array. Everything else in a real provider spec (approval-modal detection, section windows, native chat history, auto-approve modes, and so on) is additive on top of this shape — see the built-in specs under adhdev-providers/cli/*/specs/ for fuller examples once this one is working.

Why (?=\n|$) and not a bare $? A matches pattern runs against the entire on-screen buffer, not line-by-line, and the engine compiles it without the m flag by default — so an unescaped $ anchors to the end of the whole screen, not the end of the prompt line, and would silently never match while there's anything below the prompt. The spec linter whitelists a couple of no-m line-boundary idioms for exactly this reason ((?=\n|$) for line end, (?:^|\n) for line start); using one of those instead of a bare anchor is what makes this pattern actually match the prompt line reliably.

Put both files at ~/.adhdev/providers/cli/python-repl/provider.v1.json and ~/.adhdev/providers/cli/python-repl/specs/1.0.json, enable the provider first (adhdev provider enable python-repl or the dashboard's Machine Providers toggle — the dashboard's own detection check refuses a disabled provider, see below), then launch a session against it. Because python3 is already installed, there's nothing else to set up before trying it.

The example files above are also saved together, ready to copy verbatim, under custom-provider-example/ next to this guide's source.

Validating a provider ​

  • adhdev provider validate <path> checks a provider.v1.json (or legacy provider.json) against the same v1 JSON Schema the daemon uses at load time, and reports which tier it falls into. For a cli manifest it also resolves the spec the same way the daemon does at launch (compatibility[].spec → specs/default.json → spec.json) and runs it through the same FSM validator the daemon uses — a manifest that passes the schema but has no resolvable spec, or a spec with a malformed state machine, is reported as invalid here instead of only failing later at launch (formatNoResolvableSpecError). acp manifests have no spec concept and skip that step. For the example above it prints tier: extended-legacy and python-repl@unversioned — both are expected and harmless:
    • extended-legacy is simply "neither of the other two tiers": extended means the manifest declares an overrides block (custom JS hooks), declarative-only means it declares a tui block. A plain spec-routed manifest like this one — no overrides, no tui — falls into extended-legacy by elimination. It doesn't mean anything is missing.
    • unversioned just means the manifest has no providerVersion field. Nothing in the schema requires it and nothing in the loader depends on it for a CLI provider to load or run — it exists purely so provider list/validate have something to print. Add a "providerVersion": "1.0.0" field if you want a real value shown instead, but it's cosmetic for a manifest like this.
  • adhdev provider list (add --json for machine-readable output) shows every provider the daemon currently has loaded, from every source directory, with the spec version pin.
  • adhdev provider detect [type] checks whether the underlying binary is actually installed and on PATH. Run from the CLI, this deliberately bypasses the enable gate, so it works even before you've enabled the provider. The dashboard/daemon equivalent (detect_provider) is stricter: it refuses with Provider is disabled on this machine if the provider hasn't been enabled yet. So enable the provider first if you're checking detection from the dashboard rather than the CLI.
  • Editing the manifest in your user-overrides folder while the daemon is running triggers the hot-reload watcher described above — check the daemon log for File changed: provider.v1.json, reloading... and any ⚠ validation warnings.

There is no dashboard "fix" or auto-repair flow for a hand-written, out-of-tree provider like this one — that tooling targets the built-in provider bundle's own script layout, not custom manifests you maintain yourself.

Sharing a provider ​

If you'd like your provider to become part of the built-in set everyone gets automatically, contribute it upstream to github.com/vilmire/adhdev-providers — the same repository the daemon's .upstream/ bundle is built from. Open a pull request adding your cli/<type>/ directory following the repo's own contributing guide. Note that landing there makes a provider part of the built-in inventory; it doesn't automatically make it a verified provider — see Provider Issues for the difference.

Until (or instead of) that, a provider directory is just a folder — you can put it under version control, share it as a zip, or point a teammate's providerDir at the same git checkout.

Hosted cloud docs live here. Open-source and self-hosted docs live in the OSS repository.