Part of forgectl — see the command roster.
Optional. forgectl runs with sensible defaults and no config file. To persist preferences, drop a TOML file at config.toml in your OS config dir:
- macOS:
~/Library/Application Support/forgectl/config.toml - Linux:
~/.config/forgectl/config.toml
A config.toml that exists but does not parse is an error, not a fallback to defaults: every command exits 2 and names the file, line and column. So is one that exists but can't be read, such as a file you lack permission to read, a directory, or a FIFO: the error names the file and the reason. Only an absent file selects the defaults. forgectl config (alias cfg), doctor, launch edit and launch doctor, plus help, version and completion, still run so you can find and fix the file. So does resume snapshot, which runs from a Stop hook and always exits 0. init does not: it refuses to rewrite a file it cannot parse.
User workflow files share the same base: <config dir>/workflows/<name>.workflow.toml.
no_icons = false # use ASCII markers instead of Nerd Font glyphs
log_level = "off" # off | debug | info | warn | error
log_file = "" # "" = auto (daily-rotated file); "-" = stderr; or an explicit pathforgectl config (alias cfg) prints every section of the config — the host scalars, [launch] and its [launch.defaults], and every domain section down to the nested [review.gitea] — one dotted key per line, each marked (set) when the config file supplied it or (default) when it came from a built-in fallback.
That marker is the point. Every section's zero value means "absent, built-in defaults apply", so a value alone cannot tell a key you never set from a key you misspelled. The command also lists unrecognized keys — anything in the file that bound to no field, which catches probe_hostt and a key filed under the wrong section — and surfaces the decode error from a malformed file instead of silently defaulting past it. Resolved values that differ from the stored ones (the off log level, the dated log path, the launch binary chosen by FORGECTL_CLAUDE_BIN > binary_path > PATH) print in their own labeled blocks.
sessions.dsn renders as (redacted); its (set)/(default) marker is the useful signal and a connection string can carry a password. launch.defaults.env and proxy.profiles render their key names only for the same reason — they hold arbitrary environment values or credential-bearing proxy URLs. forgectl launch which applies the same policy to the launch map. proxy.launch_profile is a profile name, not a value, so it renders plain while the profile it names stays redacted.
Proxy values reach exactly two forgectl outputs, both purpose-built and neither a display surface: the proxy use shell protocol, and the environment of a harness started with proxy.launch_profile set — both described in the proxy doc. Capture the shell protocol through the wrapper rather than displaying it. Note what the second one implies: a launched agent can read its own environment, so every proxy value is readable by the harness and anything it runs. A forgectl pr review window receives the same environment as tmux -e KEY=VALUE arguments, which any local user can read on the command line, so a launch profile whose proxy URL carries user:pass@ is refused.
--json emits the same information machine-readably and is the stable surface — the human rendering may reflow.
Logging is off by default. Set log_level to debug for the full narrative (every tmux/sesh subprocess, with timing) or info for just the success/failure story. Logs follow an action-oriented pattern — Preparing to… / Successfully… / Failed to… — so they read top-to-bottom when something goes sideways.
With log_file = "" (the default target once a level is set), forgectl writes to a daily file — forgectl-YYYY-MM-DD.log — in the config dir and prunes any such file older than 7 days on startup. Set log_file = "-" to log to stderr instead, or give an explicit path to opt out of rotation.
Running forgectl pr drain --watch as a long-lived watcher is the case this matters most for. Its own one-line-per-pass summary (pass=… free=… queued=…) prints regardless of log_level — it is a plain stdout write, not a log — but that line is deliberately terse: it names the pass counts, not why a given launch failed. Set log_level = "info" (or debug for the full subprocess narrative) with an explicit log_file when running --watch unattended, so a launch failure's slog detail lands somewhere a stdout-discarding process supervisor won't drop it.
Several command groups own their own config section, documented alongside that command:
env— safe.envmanagementresume— session resume across repos;[[resume.on_update]], the hooks fired when claude updateslaunch—[launch], per-project Claude Code / Codex / Pi profilespr—[pr], the clean-room reviewer's own postureproxy—[proxy.profiles], named profiles;launch_profileapplies one to every launchprojectsandreview—[projects],[[projects.wings]],[review],[github], whose repos get enumerated and where clones landbench—[bench], interop with the local dev servicesk8s— bounded, terminal-safe log streamingdocs—[docs], local markdown readertheme—[theme],[theme.colors], the palette every styled surface draws fromherdr—[herdr.organize], the rules that group herdr tabs into workspaces
[theme] selects the palette every styled surface draws from — the TUI, huh
prompts, doctor marks, and fang's --help and error frames. Defaults to the
Artificer terminal palette, resolved dark.
[theme]
preset = "artificer" # "artificer" (default) | "legacy"
mode = "dark" # "auto" (default) | "dark" | "light"
[theme.colors]
accent = "#dbbb6f" # one hex: both modes
danger = { dark = "#e6a8a2", light = "#8a2418" } # per modemode = "auto" asks the terminal for its background, but only where that is
safe: both stdin and stdout must be a real TTY, NO_COLOR must be unset, and
TERM must not be screen* or tmux*. A multiplexer does not forward the
query, so everything renders dark inside tmux regardless of the terminal
behind it. On a light terminal inside tmux, set mode = "light" explicitly —
that is the case auto-detection cannot see.
Help and errors still query the terminal. The policy above governs
forgectl's own detection. --help, --version and error output are rendered by
fang v1.0.0, which asks the terminal
for its background (an OSC 11 query plus a DA1 request) whenever stdout is a
TTY. It does this even where forgectl's policy refuses to probe: inside
tmux/screen, with NO_COLOR set, or with a forced [theme] mode. forgectl
ignores the answer in those cases (it resolves the palette from mode or the
dark default instead), so the colours are right; only the query itself is sent.
Measured cost is under 50 ms and no hang has been reproduced. Piped output never
queries. There is no fang option to turn the query off, so this is accepted
until upstream adds one (tracked in #546).
A bad [theme] never stops the binary starting: it is reported by doctor and
launch doctor, and the default palette is used. Run forgectl theme show to
see what actually resolved, including which roles came from an override and
whether any override is inert. Role names and the contrast column are
documented in commands/theme.md.