Skip to content

Configuration reference ​

Every field of the native kranz.yaml format, with its type, default, and a usable example. For one complete file with all of it in context, see the annotated configuration.

Durations are Go duration strings: 500ms, 5s, 2m, 1h30m. A bare number is not a valid duration.

Root ​

yaml
project: Northstar
version: "1.0"
runtime: {}
defaults: {}
include: []
overrides: []
protected: {}
services: {}
action_groups: {}
ui: {}
FieldTypeDefaultDescription
projectstring—Required. Project name shown in the header
versionstring—Free-form version label for your own use
runtimemap{}Stable runtime addressing options
defaultsmap{}Execution context inherited by every service
includelist[]Autonomous local configurations to compose
overridesstring list[]Ordered partial files applied to this autonomous config
protectedmap{}Final values enforced over this config and its descendants
servicesmap{}Long-running processes and detached resources
action_groupsmap{}Project-level one-shot commands
uimap{}Appearance for this project

A configuration needs at least one service or one action group.

project ​

Type: string · Required

Displayed in the header and used to identify the project.

yaml
project: Northstar

version ​

Type: string · Default: none

A label for your own bookkeeping. Kranz neither validates nor interprets it. Quote it, or YAML reads 1.0 as a number.

yaml
version: "1.0"

runtime ​

Type: map · Default: {}

Controls the stable name used to address a live runtime from another terminal, an MCP client, or a script. When omitted, Kranz derives a lowercase slug from project.

FieldTypeDefaultDescription
namestringproject slugRuntime name shown by kranz ps and accepted by -p or KRANZ_PROJECT
yaml
project: Northstar
runtime:
  name: northstar-dev

defaults ​

Type: map · Default: {}

Execution context inherited by every service that does not set its own value. Only these four fields exist; defaults cannot set commands, probes, or ports.

FieldTypeDefaultDescription
dirstringconfig file directoryWorking directory for commands
shellstring/bin/bashShell used to run commands
envmap{}Environment variables
env_filesstring list[]Dotenv files, applied in order
yaml
defaults:
  dir: .
  shell: /bin/sh
  env:
    NODE_ENV: development
  env_files: [.env.shared]

If defaults.dir is omitted, it acts as .: the directory containing this configuration file. A service or action group without dir inherits that directory. Lifecycle commands and actions inherit their owner's directory unless they set their own dir. Relative dir values at every level resolve against the file that declares them, including when that file is included in another config.

Composition ​

Every included file is a complete, independently runnable configuration. Its project, ui, and defaults remain local; only its services and action groups are exported. Relative paths and the adjacent .env are resolved from that file's directory before it joins the effective graph.

yaml
project: Workspace
include:
  - path: repositories/catalog/kranz.yaml
  - glob: repositories/*/kranz.yaml
  - discover:
      root: tools
      max_depth: 3
      follow_symlinks: false
    max_depth: 2

An include entry sets exactly one of path, glob, or discover. Exact paths must exist. A glob that matches nothing fails with its own config_glob_empty error rather than the missing-file error, so a typo is easy to tell apart. Remote sources (URLs) are rejected. Glob and discovery results are sorted by normalized path, then canonical paths are deduplicated: a file reached twice, for example through two includes of a shared stack, loads once and is reported as source_deduplicated. An include cycle fails with config_include_cycle and the chain of relative paths.

Two different limits apply. discover.max_depth bounds the filesystem walk. The entry-level max_depth bounds the include graph below the files that entry selects: max_depth: N lets N further include levels load, and a file with no budget left still loads, but its own includes are truncated and reported as include_depth_truncated. max_depth: 0 therefore loads the selected files and none of their includes. Without max_depth, include depth is unlimited.

The selector can resolve a native Kranz file, a Procfile, or a supported process-compose.yaml. Native files may recursively include more sources. Procfile and Process Compose sources are terminal leaves: they contribute their services with paths resolved beside the leaf, but cannot declare Kranz include or overrides. A later composition-wide --override may still patch their effective services. The conventional process-compose.override.yaml is loaded with its Process Compose base for compatibility.

Discovery does not follow file or directory symlinks by default. Explicitly including a symlink is allowed. Opt in per discovery block or globally with --follow-symlinks. Hidden and dependency directories are not implicitly ignored; choose a narrow root or depth when that matters.

If the working directory has no conventional root file, Kranz discovers autonomous configs below it and creates a virtual project named after the directory. If discovery finds nothing either, commands that need a project fail with config_not_found; a bare kranz opens the runtime picker instead when a local runtime is already running.

Service names remain short when unique. Collisions receive the smallest useful directory prefix. The canonical source path plus original service key forms a stable internal ID, so later qualification of a display name cannot transfer a live process to another service. An ambiguous short selector reports the qualified display names; the display name and stable ID are always unambiguous.

Service references are resolved only after display-name allocation. A bare depends_on name first resolves within the file that declares it, so an autonomous file can keep depends_on: [api] even when another discovered file also exports api. With no local match, a bare name resolves to the one global service with that original name. Multiple global matches are an error that lists the qualified choices; zero matches are reported by dependency validation. Kranz does not use source discovery order to break a tie.

The resolver rewrites depends_on, dependency_conditions keys, and before_start[].service to effective display names together. A qualified display name can explicitly select a cross-source service, but it is derived from the current set of sources and can change when a new collision appears. Reload therefore resolves the authored references again and rejects an ambiguous or missing new graph while stable service IDs preserve process identity across display-name changes.

Override layers ​

overrides is distinct from composition. Layers apply left to right to the logical config that declares them; CLI layers use repeatable --override PATH.

yaml
overrides:
  - kranz.local.yaml
  - kranz.secrets.yaml

Where a layer is declared decides what it can see:

  • A file's own overrides: list applies to that file before it joins the composition. It patches only that file's services and names them by their original keys (api), even if the service is later shown as catalog/api.
  • --override PATH (or KRANZ_OVERRIDE) applies after the whole graph is composed, so it can patch any service and names it by display name (catalog/api). Relative CLI paths resolve against the working directory after -C.

Mappings merge by key, sequences replace, an empty mapping clears a mapping, and null removes a value so its normal default can apply. Structural type changes are errors. Override files cannot declare defaults, include, overrides, or protected; this prevents defaults from leaking between files. Relative dir values in a layer resolve beside that layer, and ${VAR} references expand from the process environment, then the .env beside the layer.

command and lifecycle.start are one field. A layer can change only lifecycle.start.confirm or a start timeout and keep the base command, and a layer that sets command keeps the base start options. Setting command: null removes the start command.

Protected values ​

protected uses the ordinary typed configuration tree and applies after local defaults and overrides. Descendant protected values apply first; each parent is applied afterwards, so the root policy wins.

yaml
protected:
  services:
    database:
      env:
        TLS_MODE: required

config show --provenance, config explain, and kranz://config expose the winning source and protected rejection without revealing secret environment values. config sources (or m in the dashboard) shows the same result per file: the include tree, the services each file defined, and every field it overrode, including protected values.

Services ​

Each key under services is a service name. Names are shown verbatim in the list, so keep them short and stable.

FieldTypeDefaultDescription
commandstring—Shorthand for lifecycle.start.command
descriptionstring—One line shown in Details
supervisionenumprocessprocess or detached
lifecyclemap{}Explicit start, stop, status, and logs
stop_on_exitbooltrue / falseWhether runtime shutdown stops this service
dirstringdefaults.dirWorking directory
shellstringdefaults.shellShell for commands
envmap{}Environment variables
env_filesstring list[]Dotenv files, applied in order
is_dotenv_disabledboolfalseSkip the adjacent automatic .env for this service
portsint list[]Declared ports, checked before start
detect_portsboolsee belowDiscover listeners at runtime
tagsstring list[]Grouping and selection labels
depends_onstring list[]Services that must start first
dependency_conditionsmap{}What "ready" means per dependency
healthcheckmapnoneReadiness and liveness probes
ready_log_lineregex—Readiness from a log line
availabilitymap{}Restart policy and project exit
shutdownmap{}How this service is stopped
actionsmap{}One-shot commands owned by this service
before_startlist[]Actions that must succeed before start
success_exit_codesint list[0]Additional successful exit codes
disabledboolfalseExclude from batch start

command ​

Type: string · Required for process supervision

The command that runs the service. It is executed by shell, so pipes, &&, and variable expansion work.

yaml
services:
  api:
    command: npm run dev

command is exactly equivalent to lifecycle.start.command, and is normalized into it before configuration layers merge. Use the explicit form when the start needs its own timeout or confirmation:

yaml
services:
  api:
    lifecycle:
      start:
        command: npm run dev
        confirm: true

A single file must use one form or the other, never both.

description ​

Type: string · Default: none

One line explaining what the service is, shown in Details.

yaml
description: Messenger API and WebSocket backend

supervision ​

Type: process | detached · Default: process

Declares where lifecycle truth comes from.

  • process — Kranz starts the command, owns its process group, and knows the service stopped when the process exits.
  • detached — the start command finishes while the resource it created keeps running. There is no PID to supervise, so stop and status must be described explicitly.
yaml
services:
  remote-stack:
    supervision: detached

See the lifecycle guide for the full model.

lifecycle ​

Type: map · Default: {}

FieldTypeApplies toDescription
startaction shapebothHow to start
stopaction shapedetachedHow to stop
statusstatus shapedetachedWhether the resource exists
logsaction shapedetachedA command that streams logs

A process-supervised service may only declare lifecycle.start; Kranz already knows how to stop, observe, and read the logs of a process it owns. A detached service may declare any subset: one with only status is observe-only, and its start and stop controls stay unavailable rather than pretending to work.

Lifecycle action shape ​

start, stop, and logs share this shape:

FieldTypeDefaultDescription
commandstring—Required
descriptionstring—Human-readable intent
dirstringservice dirWorking directory
shellstringservice shellShell
envmapservice envAdded and overriding variables
env_filesstring listservice env_filesDotenv sources
timeoutdurationnoneDeadline for the whole command
confirmboolfalseAsk before running (start only)

Lifecycle commands cannot be interactive. Stopping from the TUI always asks for confirmation regardless of confirm.

yaml
lifecycle:
  start:
    command: ssh host 'cd app && docker compose up -d'
    timeout: 2m
  stop:
    command: ssh host 'cd app && docker compose down'
    timeout: 2m

timeout covers the entire command including the remote side of an SSH call.

Lifecycle status ​

A status probe answers one question: does the external resource exist and run? It is not a health check and never restarts anything.

FieldTypeDefaultDescription
typeenum—Required. Currently only command
commandstring—Required. Observation command
initial_delayduration0sWait before the first probe
intervalduration5sPoll period while running
stopped_intervalduration30sPoll period while stopped or unknown
timeoutduration2sDeadline for one probe
failure_thresholdint3Unclassified probes before unknown
running_exit_codesint list[0]Codes meaning running
stopped_exit_codesint listunsetCodes meaning stopped

Exit code contract. By default, exit 0 means running and every other exit code means stopped — the same convention every shell command already follows:

yaml
status:
  type: command
  command: docker compose ps --status running --quiet api | grep -q .

Declaring stopped_exit_codes opts into a three-way contract, for probes that can also report "I could not tell". Codes in neither set are unclassified, and after failure_threshold consecutive unclassified results the service is shown as unknown:

yaml
status:
  type: command
  # 0 = running, 3 = stopped, 4 = the host is unreachable
  command: ssh host ./stack-status.sh
  running_exit_codes: [0]
  stopped_exit_codes: [3]
  failure_threshold: 2

A probe that never produced an exit code at all — it could not start, timed out, or was killed — is always unclassified, never "stopped". Running and stopped sets must not overlap, and each code must be between 0 and 255.

stop_on_exit ​

Type: bool · Default: true for process, false for detached

Whether stopping the runtime also stops this service. Detaching a TUI does not stop the runtime and therefore does not apply this setting.

yaml
services:
  remote-stack:
    supervision: detached
    stop_on_exit: false

Process-supervised services always stop with Kranz and cannot set false; Kranz does not leave orphaned child processes behind. A detached resource defaults to surviving runtime shutdown, which is usually what you want for external infrastructure shared with other tools. The quit dialog lists exactly what shutdown will stop and what it will leave running.

dir, shell ​

Type: string · Default: defaults.dir (config file directory), defaults.shell (/bin/bash)

yaml
services:
  api:
    dir: apps/api
    shell: /bin/bash

Relative directories resolve against the configuration file. An omitted dir inherits defaults.dir, which is . beside that file if also omitted. Details shows the path relative to where Kranz is running.

env, env_files ​

Type: map, string list · Default: {}, []

yaml
services:
  api:
    env_files: [.env, .env.local]
    env:
      PORT: "3001"

Precedence, lowest to highest:

  1. .env beside the configuration file that declares the service
  2. defaults.env
  3. defaults.env_files, in order
  4. service env_files, in order
  5. service env

A value already present in your shell environment wins over the adjacent .env, but explicit configuration values always win. $HOME-style references expand after all layers merge. In a composed project each file's .env reaches only the services that file declares. Referenced dotenv files are watched; in the TUI, press Ctrl+L to apply a detected change.

is_dotenv_disabled ​

Type: bool · Default: false

Set this on a service when it must not inherit the automatically discovered .env beside the configuration file that declares it. Explicit defaults.env, defaults.env_files, service env_files, and service env still apply.

yaml
services:
  worker:
    command: npm run worker
    is_dotenv_disabled: true

ports ​

Type: int list · Default: []

Ports this service is expected to listen on.

yaml
services:
  api:
    ports: [3001]

Declared ports are checked before start. If another process holds one, Kranz names the owner and asks what to do instead of failing obscurely. They are also valid documentation for a detached resource whose ports live elsewhere.

detect_ports ​

Type: bool · Default: true when ports is empty, otherwise false

Discover TCP listeners actually opened by the service and its children.

yaml
services:
  vite:
    detect_ports: true

Useful when a dev server picks its own port. Details separates declared ports from detected ones. Not available for detached services, which have no local process group to inspect; setting true there is rejected. See logs, runs, and ports.

tags ​

Type: string list · Default: []

yaml
tags: [backend, messenger]

Tags appear as expandable groups with their own summary Details. Selecting a tag selects every service in it.

depends_on ​

Type: string list · Default: []

yaml
services:
  web:
    depends_on: [api]

Starting a service starts its dependencies first. Stopping it stops its dependents first, in reverse order. Shift+S overrides both and acts only on the selection.

dependency_conditions ​

Type: map · Default: process_healthy per dependency

What counts as "ready" for each dependency.

ConditionMeaning
process_startedThe process exists
process_healthyIts readiness probe passes (default)
process_completedIt finished, with any exit code
process_completed_successfullyIt finished successfully
process_log_readyIts ready_log_line matched
yaml
services:
  web:
    depends_on: [api, seed]
    dependency_conditions:
      api: {condition: process_healthy}
      seed: {condition: process_completed_successfully}

process_log_ready requires the dependency to define ready_log_line.

healthcheck ​

Type: map · Default: none

Two independent probes:

  • readiness — may dependents start? Gates the dependency graph.
  • liveness — is this running service still healthy? Surfaces unhealthy.
yaml
healthcheck:
  readiness:
    type: http
    url: http://127.0.0.1:3001/ready
    interval: 2s
  liveness:
    type: tcp
    port: 3001
    initial_delay: 10s
    interval: 15s

Each probe accepts:

FieldTypeDefaultApplies to
typehttp | tcp | command—Required
urlstring—http
headersmap{}http
status_codeintany 2xxhttp
portint—tcp
commandstring—command
port_fromdetected—http, tcp
detected_port_indexint0http, tcp
initial_delayduration0sall
intervalduration5sall
timeoutduration2sall
failure_thresholdint3all

An empty healthcheck block is invalid — a probe with no type cannot be run, and silently ignoring it would misreport the service as healthy.

To probe a port discovered at runtime instead of a fixed one, take the port from discovery. A tcp probe with no port, or an http probe with port_from: detected, targets the first detected listener; detected_port_index selects a later one for a service that opens several. See health and dependencies.

ready_log_line ​

Type: regular expression · Default: none

Readiness from output, for services with no endpoint to probe.

yaml
services:
  worker:
    command: npm run worker
    ready_log_line: "worker listening"

Cannot be combined with a readiness probe; declare one source of readiness.

availability ​

Type: map · Default: {}

FieldTypeDefaultDescription
restartno | always | on_failure | exit_on_failurenoRecovery policy
backoffduration0sWait before restarting
max_restartsint0 (unlimited)Restart attempt limit
exit_on_endboolfalseEnd the runtime when this service ends
exit_on_skippedboolfalseEnd the runtime if a dependency gate skips it
yaml
availability:
  restart: on_failure
  backoff: 2s
  max_restarts: 3

Restart policies apply to process-supervised services only. Kranz does not restart a detached resource it does not own.

shutdown ​

Type: map · Default: {}

FieldTypeDefaultDescription
signalint15 (SIGTERM)Signal sent first
timeoutduration3sGrace period before escalation
commandstring—Custom graceful shutdown command
parent_onlyboolfalseSignal only the leader, not the group
yaml
shutdown:
  signal: 15
  timeout: 10s

After timeout expires the process group is killed, so a service that ignores SIGTERM still exits.

By default the whole process group is signaled, so child processes do not survive their parent. Use parent_only: true only when a program manages its own children and must handle shutdown itself.

actions ​

Type: map · Default: {}

One-shot commands owned by this service. See actions below and the actions guide.

yaml
services:
  api:
    command: npm run dev
    actions:
      migrate:
        command: npm run db:migrate
        confirm: true

before_start ​

Type: list · Default: []

Actions that must succeed before this service starts. Each entry references an existing action rather than inlining a command, so the same command stays runnable and inspectable on its own.

FieldTypeDefaultDescription
actionstring—Required. Action name
servicestringthe declaring serviceService owning the action
groupstring—Action group owning the action
runonce | alwaysonceHow often it runs per session
yaml
services:
  api:
    command: npm run dev
    actions:
      migrate:
        command: npm run db:migrate
    before_start:
      - action: migrate
      - group: infrastructure
        action: up
        run: always

Set either service or group, not both. Prerequisites run in declared order, after dependencies are ready and before the service starts. once means one successful run per Kranz session, including across restarts; always runs before every start. If a prerequisite fails, the service stays stopped and the failure is reported in its logs. Interactive actions cannot be prerequisites.

success_exit_codes ​

Type: int list · Default: [0]

Additional exit codes treated as success, for commands that report a meaningful non-zero status.

yaml
success_exit_codes: [0, 2]

disabled ​

Type: bool · Default: false

The service stays visible and startable by hand. Pressing a does not select it, and start-all skips it, so it is excluded from a-style batch starts.

yaml
disabled: true

Action groups ​

Project-level actions that belong to no single service.

yaml
action_groups:
  infrastructure:
    description: Shared development infrastructure
    dir: infra
    env_files: [.env.infra]
    actions:
      up:
        command: docker compose up -d
      reset:
        command: docker compose down --volumes
        confirm: true
FieldTypeDefaultDescription
descriptionstring—Shown on the group row
dirstringdefaults.dirInherited working directory
shellstringdefaults.shellInherited shell
envmap{}Inherited environment
env_filesstring list[]Inherited dotenv sources
actionsmap—Required. The group's actions

An action's own values override the group's.

Action fields ​

The same shape for service actions and group actions:

FieldTypeDefaultDescription
commandstring—Shell command; required unless run, argv, or params is set
descriptionstring—What it does, shown beside the name
dirstringowner's dirWorking directory
shellstringowner's shellShell
envmapowner's envAdded and overriding variables
env_filesstring listowner's env_filesDotenv sources
timeoutdurationnoneDeadline for the whole process group
confirmboolfalseAsk before running
interactiveboolfalseHand the terminal to the command
runstring list—Fixed argument vector; parameter projections are appended
argvstring list—Explicit argument vector with {{name}} placeholders
paramsmap{}Typed parameters for a parameterized action

The action's key is its name in the list; description explains it. Keep keys short and stable — migrate, not run-the-database-migrations.

Parameterized actions ​

A parameterized action declares typed controls under params and either a fixed run (projections appended in declaration order) or an explicit argv template. The command runs without a shell, so a value stays one process argument and can never become shell syntax.

yaml
action_groups:
  infra:
    actions:
      seed:
        params:
          env:
            type: select
            options: [dev, staging, prod]
            default: dev
            prompt: Target environment
          count:
            type: number
            min: 1
            max: 10000
            default: 100
          dry_run:
            type: checkbox
            default: true
            env: DRY_RUN
            confirm: This can modify stored data
        run: [./seed.sh]
        env:
          ENV_NAME: "{{env}}"

Controls: checkbox (boolean, or an array when options is set), radio and select (one string), text (string), number (integer).

Each parameter declares exactly one of default, required: true, or optional: true, and at most one projection:

ProjectionMeaningExample result
flag: --cleanboolean true adds the flag--clean
arg: --clientflag and value as two elements--client all
arg: --target=joined into one element--target=phone
positional: truethe value alonedeploy
env: DRY_RUNenvironment variableDRY_RUN=1

An options list may instead map each value to an option node carrying flag, args, label, confirm, or its own run. When every option of a radio or select carries run, the selected option supplies the whole command.

In an argv template, {{name}} means one of two things by position. An element that is nothing but the placeholder inserts that parameter's whole projection, which is zero, one, or several arguments; the same placeholder inside surrounding text inserts the value as a literal:

yaml
argv: ["./tool", "subcommand", "{{verbose}}", "--label=build-{{target}}"]

With verbose projecting flag: --verbose and target holding phone, that renders ./tool subcommand --verbose --label=build-phone. Values are substituted once: a value that itself contains {{...}} stays literal.

A $ in any configuration value is expanded from the environment and the adjacent .env before the file is parsed. Write $$ for a literal dollar, for example a shell loop variable inside a command.

Placeholders belong in argv. A {{name}} in run is a configuration error, because run is a fixed vector whose projections are appended after it. argv[0] is the executable and takes no placeholder, and an action that declares run, argv, or params cannot declare shell: it never reaches one.

Prerequisite params are static values checked when the configuration loads. before_start entries gain one field:

FieldTypeDefaultDescription
paramsmap{}Static values for the referenced action's parameters

UI ​

Appearance for this project. Personal settings live outside the repository; see appearance.

FieldTypeDefaultDescription
themestringbuilt-in defaultNamed theme
accent#RRGGBBtheme accentAccent color
backgroundterminal | theme | #RRGGBBterminalCanvas source
color_modeauto | dark | lightautoPalette mode
yaml
ui:
  theme: tokyo-night
  accent: "#7AA2F7"
  background: terminal
  color_mode: auto

Ctrl+T opens the live picker, which can write these values back to the project or to your personal settings.

Validation rules ​

Kranz rejects a configuration rather than starting with an ambiguous one:

  • unknown fields are errors, so a typo never becomes silence;
  • a process-supervised service needs command or lifecycle.start;
  • lifecycle.stop, status, and logs require supervision: detached;
  • detect_ports: true and restart policies are rejected for detached services;
  • running_exit_codes and stopped_exit_codes must not overlap;
  • every probe needs its own type;
  • ready_log_line and a readiness probe are mutually exclusive;
  • dependencies must exist, and the graph must be acyclic;
  • before_start must reference an action that exists and is not interactive.

An invalid reload leaves the accepted configuration in place. Additions and stopped-service changes apply when a reload succeeds. A changed or renamed running service keeps its accepted snapshot and is reported as pending_restart; an explicit restart adopts the desired snapshot. Removing a running service in the TUI requires a stop-or-keep-running choice. Keeping it running removes it from the configured service list while the process remains active until stopped separately.

Released under the MIT License.