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):
- Upstream —
<configDir>/providers/.upstream/— the official, auto-synced bundle. Don't edit this by hand; it gets overwritten. - External sources —
<configDir>/external/<source-name>/— 3rd-party git sources you register from the dashboard's Sources panel. - 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 (
normalorno-upstream) and Provider directory, then Apply & reload.no-upstreamdisables the auto-synced upstream bundle entirely and serves only your own directory plus whatever's inexternal/. - Config file: set
providerDir(and optionallyproviderSourceMode) 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.logregardless 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:
adhdev provider init my-cli --dir ./my-cliThis 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
{
"$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
{
"$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$? Amatchespattern runs against the entire on-screen buffer, not line-by-line, and the engine compiles it without themflag 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-mline-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 aprovider.v1.json(or legacyprovider.json) against the same v1 JSON Schema the daemon uses at load time, and reports which tier it falls into. For aclimanifest 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).acpmanifests have no spec concept and skip that step. For the example above it printstier: extended-legacyandpython-repl@unversioned— both are expected and harmless:extended-legacyis simply "neither of the other two tiers":extendedmeans the manifest declares anoverridesblock (custom JS hooks),declarative-onlymeans it declares atuiblock. A plain spec-routed manifest like this one — nooverrides, notui— falls intoextended-legacyby elimination. It doesn't mean anything is missing.unversionedjust means the manifest has noproviderVersionfield. 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 soprovider list/validatehave 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--jsonfor 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 onPATH. 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 withProvider is disabled on this machineif 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.
Related docs
- Provider Issues — what to do when a built-in provider misbehaves.
- CLI Agents — using the CLI agents ADHDev already supports.
- Compatibility & Caveats
