Skip to content

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
# yaml-language-server: $schema=https://raw.githubusercontent.com/amr-m-abdelgawad/devctl/main/schema/devctl.config.schema.json
version: 1

The schema file lives at schema/devctl.config.schema.json. The demo config uses a relative path so it works offline.

Discovery ​

Walks from the current directory toward the filesystem root:

--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:

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.

Overlays and precedence ​

The selected config is assembled in this order. Each stage overrides fields provided by an earlier stage, so the rightmost source wins:

--config selects the main config file (or a .devctl directory); it is not an override layer. ENV_SOURCE_ORDER controls the separate service-process environment merge described in Environment.

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).

Top-level keys ​

KeyRole
versionMust be 1
project.nameShown in the TUI header
google.project_id / regionCloud project (optional)
templatesNamed service bases (extends)
servicesProcess definitions
tasksNamed transient commands run with devctl run
httpNamed outbound HTTP recipes — see Custom HTTP APIs
profilesNamed service sets, overlay binds, and per-service env
proxyListen address, token endpoint, routes (inspect.enabled captures bodies) — see Proxy
logsIn-memory cap and persistence
telemetry.otlpOpt-in loopback OTLP/HTTP receiver, JSON or protobuf (off by default) — see Telemetry
telemetry.store_max_bytesByte budget for the trace store, proxy request spans included (64 MiB when unset) — see Telemetry
webOpt-in loopback telemetry web UI (off by default, port 18900) — see Web console
llmOpt-in LLM traffic inspector (off by default) — see LLM inspector
auth.refresh_threshold_secondsToken refresh window (default 300)
shutdownstop_services_on_exit, grace_seconds
uiOptional theme / keymap hints in YAML (TUI prefs still win from tui.json)
secretsredact (default true), plus extra redaction markers and regexes. secrets.redact: false stops masking newly captured logs, traffic, and LLM payloads
doctor.toolsExtra CLI binaries to probe
plugins{ path } modules loaded when the supervisor starts
environment.sources / secrets / sopsEnv source order, named secrets, and an optional SOPS file. A service can also set environment.terraform or environment.helm to read literal env vars from its Terraform or Helm YAML — see Environment

Templates ​

yaml
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 ​

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. A service hooks.pre_start or hooks.post_start command can set environment the same way; see Services.

Validation and reload ​

bash
devctl config validate
devctl config validate --json
devctl config show
devctl config diff
devctl reload

config 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>).

devctl config diff — each effective value with the file and layer that won

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 TUI Config screen — merged project, google, runtime, logs, proxy routes, services, and tasks in one view

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.

Released under the MIT License. Use at your own risk.