-
Notifications
You must be signed in to change notification settings - Fork 0
configuration
devctl is driven by YAML. Unknown fields are rejected. version: 1 is required.
Editors and agents: point at the JSON Schema so field names complete.
# yaml-language-server: $schema=https://raw.githubusercontent.com/amr-m-abdelgawad/devctl/main/schema/devctl.config.schema.json
version: 1The schema file lives at schema/devctl.config.schema.json. The demo config uses a relative path so it works offline.
Walks from the current directory toward the filesystem root:
flowchart TB
cwd["Current directory"] --> nested{"Has .devctl/config.yaml?"}
nested -->|yes| root["Repo root = that directory"]
nested -->|no| single{"Has devctl.yaml?"}
single -->|yes| root
single -->|no| top{"Filesystem root?"}
top -->|no| parent["Walk to parent"]
parent --> nested
top -->|yes| miss["No configuration found"]
--config points at a file or a .devctl directory. Repository root is the directory that contains .devctl (or the parent of devctl.yaml).
When the main file lives in .devctl/, modular files merge in:
flowchart TB
main[".devctl/config.yaml"] --> services[".devctl/services/*.yaml"]
main --> profiles[".devctl/profiles/*.yaml"]
main --> http[".devctl/http/*.yaml"]
main --> routes[".devctl/proxy/routes.yaml"]
Service, profile, and HTTP recipe filenames become keys (identity.yaml → service identity, login.yaml → http.login).
Files within each modular directory are loaded in sorted filename order, making
overrides deterministic even when both .yaml and .yml fragments resolve to
the same key.
Local overlays merge after the repo config. Each stage overrides the one before it, so the rightmost source wins:
flowchart LR
defaults["Built-in defaults"] --> repo["Repository .devctl"]
repo --> homeLocal["~/.devctl/config.local.yaml"]
homeLocal --> repoLocal[".devctl/config.local.yaml"]
repoLocal --> session[".devctl/overlays/<name>.yaml"]
session --> env["DEVCTL_* / ENV_SOURCE_ORDER"]
env --> flags["CLI --config"]
The repository's own config.local.yaml overrides the one in your home
directory, not the other way round: overlays are applied home-first so the
repo-specific file gets the last word.
devctl start --overlay <name> then applies .devctl/overlays/<name>.yaml
(same keys as config.local.yaml, presence-aware) after both local files
so the session layer wins. The name is sticky for the session — omit
--overlay on later starts to keep it — and is recorded in state.json
(config_overlay) so daemon replacement and devctl reload keep applying it.
A missing file fails start and devctl config validate with
overlay "X" not found: .devctl/overlays/X.yaml. Operators may commit named
overlays; they are not a second config language.
TUI appearance is not this file. Theme, keys, mouse, and MCP listen live in tui.json layers — see Building from source and TUI. Settings can patch web.enabled, web.listen.port, proxy.inspect_max_bytes, and llm.capture_max_bytes into .devctl/config.local.yaml (created if missing; other keys are left alone).
| Key | Role |
|---|---|
version |
Must be 1
|
project.name |
Shown in the TUI header |
google.project_id / region
|
Cloud project (optional) |
templates |
Named service bases (extends) |
services |
Process definitions |
tasks |
Named transient commands run with devctl run
|
http |
Named outbound HTTP recipes — see Custom HTTP APIs |
profiles |
Named service sets, overlay binds, and per-service env |
proxy |
Listen address, token endpoint, routes (inspect.enabled captures bodies) — see Proxy
|
logs |
In-memory cap and persistence |
telemetry.otlp |
Opt-in loopback OTLP/HTTP+JSON receiver (off by default) — see Telemetry |
web |
Opt-in loopback telemetry web UI (off by default, port 18900) — see Web console |
llm |
Opt-in LLM traffic inspector (off by default) — see LLM inspector |
auth.refresh_threshold_seconds |
Token refresh window (default 300) |
shutdown |
stop_services_on_exit, grace_seconds
|
ui |
Optional theme / keymap hints in YAML (TUI prefs still win from tui.json) |
secrets |
Extra redaction markers and regexes |
doctor.tools |
Extra CLI binaries to probe |
plugins |
{ path } modules loaded when the supervisor starts |
environment.sources / secrets / sops
|
Env source order, named secrets, and an optional SOPS file. A service can also set environment.terraform to read literal env vars from its Terraform — see Environment
|
templates:
python-http:
health: { type: http, interval_seconds: 2, timeout_seconds: 1 }
logs: { stdout: true, stderr: true }
restart: { policy: on_failure, max_retries: 2, backoff_seconds: 1 }
services:
api:
extends: python-http
command: [python3, main.py]Tasks accept command, shell, working_dir, dependencies, and environment. Dependencies name services and are made ready before the one-off command runs. Tasks have no ports, health checks, restart policy, or status entry; see Services for an example.
devctl config validate
devctl config validate --json
devctl config show
devctl config diff
devctl reloadconfig diff explains the resolved result instead of merely printing it. Each
entry includes the winning source file and layer (main, modular_service,
modular_profile, modular_proxy, home_local, repo_local,
session_overlay, or synthesized) and the ordered sources it shadowed. Use
--json for structured output. config validate|show|diff apply the sticky
session overlay (or --overlay <name>).

Checks: YAML syntax, required fields, unknown fields, service references, dependency conditions and cycles, HTTP recipes (url, body vs form, reserved outputs, expose requires proxy, recipe cycles, ${http.*} / ${token} refs), health thresholds, duplicate ports, identities, proxy routes (including per-service proxy fragments and synthesized expose / http.*.expose routes merged at load), proxy.listen.port when proxy.enabled is true, environment references, profile references, optional plugins[].path, telemetry.otlp.listen (loopback host, valid port, no collision with the proxy/token-endpoint/gRPC-route ports), and web.listen (loopback host, valid port, no collision with the proxy/token-endpoint/OTLP/gRPC-route ports).
The TUI Config screen v / /buffer overlay validates this text before writing. Invalid YAML is not saved. e still opens $EDITOR.

The supervisor watches .devctl/ (fs.watch, ~200ms debounce) and runs the same path as /reload. devctl reload and TUI /reload re-read configuration, publish ConfigurationChanged, and list services that must restart because command, environment, ports, identity, or watch changed. A running proxy hot-swaps routes when listen addresses are unchanged — see Proxy.
Changing the plugins path list hot-applies token providers, log parsers, and proxy middleware. Editing an already-imported plugin file (same path, newer mtime) still requires devctl down && devctl start — Bun’s module cache cannot unload it. A running service’s environment is unchanged until that service restarts.
Start
- How it fits together
- Installation
- Quick start
- Onboard your repository
- Examples & recipes
- Developer setup
- Agent skills
Use
Configure
Identity
Reference