On this page

Operate Shoal

Configuration and prompt

Layer Shoal configuration safely, tune history and rendering, and build a fast structured prompt.

Status
Current implementation, with wiring gaps called out
For
Interactive users and administrators
On this page
  1. Main configuration precedence
  2. A practical user configuration
  3. Validation and diagnostics
    1. Unknown keys are warnings
    2. Type mismatches are hard errors
    3. Malformed TOML is a hard error
    4. Semantic constraints
  4. Main configuration reference
    1. Adapter-directory caveat
  5. Environment overrides
  6. Rich prompt configuration
    1. Prompt precedence is slightly different
  7. Format placeholders
  8. Module configuration map
  9. Prompt performance model
  10. Troubleshooting configuration

Shoal has two related configuration loaders. The main loader controls shell behavior; the prompt loader understands a richer prompt schema and also reads a dedicated prompt.toml. Knowing where they intentionally differ prevents most surprising configurations.

Main configuration precedence🔗

The main loader deep-merges four layers from lowest to highest precedence:

  1. /etc/shoal/shoal.toml
  2. $XDG_CONFIG_HOME/shoal/shoal.toml, or ~/.config/shoal/shoal.toml
  3. the nearest .shoal.toml found by walking from the current directory toward the filesystem root
  4. supported environment overrides

Only the nearest project file participates. Tables merge key by key, so a project can replace history.max_entries without copying the rest of [history].

flowchart TB
accTitle: Configuration and prompt precedence
accDescr: Core configuration merges system, user, project, and environment layers; prompt configuration then applies its theme and prompt-specific layers.
    subgraph Core["Core configuration"]
      S["system shoal.toml"] --> M["deep merge"]
      U["user shoal.toml"] --> M
      P["nearest ancestor .shoal.toml"] --> M
      E["SHOAL_* and NO_COLOR"] --> M
      M --> V["validate typed runtime config"]
    end
    subgraph Prompt["Prompt configuration"]
      T["built-in theme"] --> PM["prompt merge"]
      V --> PM
      PP["prompt-specific project settings"] --> PM
      PM --> R["prompt snapshot and renderer"]
    end

A missing file is normal. Malformed TOML, a wrong value type, or an invalid constrained value is a startup error. Unknown keys produce warnings, including a spelling suggestion when one is close enough; they are not accepted silently.

A practical user configuration🔗

Put this at $XDG_CONFIG_HOME/shoal/shoal.toml (usually ~/.config/shoal/shoal.toml) and trim it to taste:

version = 1

[history]
enabled = true
max_entries = 20000
dedup = true
ignore_space = true
ignore = ["token *", "secret *"]

[render]
color = true
paging = "auto"
pager = "less -R"
echo = "quiet"

[editor]
mode = "emacs"
bracketed_paste = true

[completion]
fuzzy = true
case_insensitive = true
menu = true
max_results = 100

[aliases]
gs = "git status"
la = "ls --all"

[env]
EDITOR = "nvim"
PAGER = "less -R"

[init]
files = ["~/.config/shoal/interactive.shl"]

Aliases are parsed as Shoal source at startup, rather than pasted into a Bourne shell. Environment entries populate Shoal’s session environment. Init files run in order at the start of an interactive session; keep machine-dependent setup in the user file and repository conventions in .shoal.toml.

Validation and diagnostics🔗

Configuration loading distinguishes warnings from hard errors. This matters operationally: an unknown future/typo key does not prevent startup, while a value of the wrong type or an invalid constrained value does.

Unknown keys are warnings🔗

Every key actually present in a loaded file is checked against the generic configuration schema. An unrecognized dotted path is preserved as a warning with its source file and, when a sibling key is sufficiently close, a spelling suggestion:

/home/dev/.config/shoal/shoal.toml: unknown config key `editor.bracketde_paste` (did you mean `editor.bracketed_paste`?)

When no candidate is close enough, no misleading suggestion is invented. Rich prompt keys under [prompt] are a known exception in practice: the generic loader only formally knows prompt.template, while the separate prompt loader understands the larger schema. Put rich configuration in prompt.toml to avoid those warnings.

Type mismatches are hard errors🔗

The loader reports the exact dotted key and expected shape before falling through to a generic deserializer message:

/home/dev/.config/shoal/shoal.toml: history.max_entries: expected a non-negative integer, found string

Array and map locations include their precise index/key, such as adapters.dirs[1] or aliases.gs. Existing files that cannot be read are also errors; a file that does not exist is simply an absent layer.

Malformed TOML is a hard error🔗

Syntax errors retain TOML’s line/column diagnostic and are prefixed with the source path. User-controlled config parsing returns a structured error rather than panicking.

Semantic constraints🔗

Values that parse and have the right TOML type still must satisfy these rules:

KeyRequirement
versionexactly 1
history.max_entriesgreater than zero
editor.modeemacs or vi
render.pagingnever or auto
render.echoquiet, commands, or all when present
completion.max_resultsgreater than zero
alias namesnonempty and contain no whitespace
environment namesnonempty
every history.ignore patternnonempty

Representative messages are stable and actionable:

version: unsupported config version 2 (expected 1)
history.max_entries: must be greater than 0
editor.mode: must be `emacs` or `vi`
aliases: alias name `g s` must not contain whitespace

The main loader validates the [reef] containers loosely enough for Reef to reparse its richer tool/runner grammar. Consult Reef tool resolution for that contract.

Main configuration reference🔗

The table below reflects the schema and defaults in the current code. An empty “runtime note” means the setting is wired on the relevant shell surface.

KeyType and defaultRuntime note
versioninteger, 1Configuration format version.
prompt.templatestring, "{cwd}"Legacy prompt form; prefer prompt.toml.
history.enabledboolean, trueInteractive history only.
history.max_entriesinteger, 10000Caps retained file entries.
history.pathpath, platform defaultExplicit history file.
history.dedupboolean, trueSkips a line identical to the immediately previous entry.
history.ignorestring list, emptyWhole-line shell-style */? patterns excluded from history.
history.ignore_spaceboolean, trueA line beginning with a space is not recorded.
render.colorboolean, trueNO_COLOR can disable it.
render.widthinteger or absentSchema-complete, but not presently applied by the shell renderer.
render.paging"never" or "auto", "never"Paging is interactive and TTY-sensitive.
render.pagercommand string or absentFalls back to $PAGER, then less -R.
render.echo"quiet", "commands", "all", or absentSurface default: all in the REPL, quiet for scripts and -c.
editor.mode"emacs" or "vi", "emacs"Selects the interactive editing mode.
editor.bracketed_pasteboolean, trueControls paste handling.
editor.keybindingschord-to-action table, emptyExtends or replaces actions for named chords.
completion.fuzzyboolean, trueAllows non-prefix matching.
completion.case_insensitiveboolean, trueApplies to candidate matching.
completion.max_resultsinteger, 100Bounds the candidate set.
completion.menuboolean, truefalse enables quick/shared-prefix completion, but a popup can still appear when ambiguous candidates have no common prefix.
adapters.dirspath list, emptyAdds adapter catalogs; see the caveat below.
init.filespath list, emptyInteractive startup scripts, in order.
aliasesstring table, emptyPersistent session aliases.
envstring table, emptyInitial session environment.
reef.toolsconstraint table, emptyUser-scope Reef constraints.
reef.runnersrunner table, emptyUser-scope script runners.
reef.options.hermeticboolean, falseRemoves the ambient PATH tail when engaged.
kernel.enabledboolean, truePresent in the schema but does not route the standalone REPL through the kernel.
kernel.sessionstring, "default"Present in the schema but not used to attach the standalone REPL.
journal.enabledboolean, trueNot currently honored by the local REPL, which opens its journal directly.
journal.state_dirpath or absentNot currently used by the local REPL journal path.
leash.policypath or absentSchema-complete; the main shell config does not currently load this policy.

history.dedup compares with the immediately preceding entry, including the last entry read from a previous session. An environment name that is nonempty but cannot be expressed as a Shoal identifier can pass generic validation yet produce a startup warning when the host tries to apply it.

The standalone REPL and the kernel are distinct hosts. Kernel process flags and token policy are documented in Agents, kernel, and MCP; durable execution history is covered in Filesystem, jobs, and history.

Adapter-directory caveat🔗

adapters.dirs is intended as an ordered extension path, and later entries can shadow earlier definitions. In the current implementation, loading a configured directory replaces the evaluator’s active adapter catalog at each step. Consequently, the last successfully loaded custom directory can become the active catalog rather than merely augmenting the bundled one. Completion may still show names from all discovered catalogs. Treat a custom directory as a complete catalog until this is corrected, and use ^command when you deliberately want to bypass adapters.

Environment overrides🔗

Boolean overrides accept 1, true, yes, or on for true, and 0, false, no, or off for false, case-insensitively.

Environment variableConfiguration target
NO_COLORdisables render.color
SHOAL_PROMPT_TEMPLATEprompt.template
SHOAL_PROMPTlegacy prompt.template; also a deprecated rich-prompt left-format alias
SHOAL_HISTORY_ENABLED / SHOAL_HISTORYhistory.enabled
SHOAL_HISTORY_MAX_ENTRIEShistory.max_entries
SHOAL_HISTORY_FILEhistory.path
SHOAL_HISTORY_DEDUPhistory.dedup
SHOAL_RENDER_COLORrender.color
SHOAL_RENDER_WIDTHrender.width
SHOAL_RENDER_PAGINGrender.paging
SHOAL_RENDER_PAGERrender.pager
SHOAL_RENDER_ECHOrender.echo
SHOAL_EDITOR_MODEeditor.mode
SHOAL_EDITOR_BRACKETED_PASTEeditor.bracketed_paste
SHOAL_KERNEL_ENABLED / SHOAL_KERNELkernel.enabled
SHOAL_KERNEL_SESSIONkernel.session
SHOAL_JOURNAL_ENABLEDjournal.enabled
SHOAL_LEASH_POLICYleash.policy
SHOAL_COMPLETION_FUZZYcompletion.fuzzy
SHOAL_COMPLETION_CASE_INSENSITIVEcompletion.case_insensitive
SHOAL_COMPLETION_MAX_RESULTScompletion.max_results
SHOAL_COMPLETION_MENUcompletion.menu

Avoid SHOAL_PROMPT: the two loaders retain different legacy meanings for it. Use SHOAL_PROMPT_LEFT for the rich prompt or SHOAL_PROMPT_TEMPLATE for the legacy template.

Rich prompt configuration🔗

The safest place for the current prompt schema is:

$XDG_CONFIG_HOME/shoal/prompt.toml

Its root is the prompt configuration itself—do not wrap it in [prompt].

theme = "rich"
nerd_font = "auto"
unicode = true
right_prompt_on_last_line = false

[format]
left = "$directory$git_branch$git_status$reef$character"
right = "$cmd_duration $jobs $time"
continuation = "... "
transient = "$character "

[transient]
enabled = false

[budget]
render_deadline_ms = 5
warn_on_exceed = true

[style]
ok = "green bold"
error = "red bold"
warn = "yellow bold"
info = "blue"
muted = "8"
accent = "purple"

[module.directory]
repo_relative = true
truncate_to = 3
truncate_style = "middle"
style = "cyan bold"

[module.cmd_duration]
min_ms = 500

[module.time]
format = "%H:%M:%S"
style = "dim"

Built-in themes are default, minimal, and rich. A theme is the lowest-precedence prompt layer; your explicit settings override it.

Prompt precedence is slightly different🔗

The prompt loader merges these sources from low to high:

  1. the named built-in theme
  2. /etc/shoal/shoal.toml’s [prompt] table
  3. the user shoal.toml’s [prompt] table
  4. the user prompt.toml root
  5. ./.shoal.toml’s [prompt] table in the exact startup directory
  6. prompt environment overrides

Unlike main project configuration, the prompt loader currently checks only cwd/.shoal.toml; it does not walk ancestors. Rich keys placed under [prompt] in shoal.toml may also trigger false “unknown key” warnings from the generic configuration schema, which formally knows only prompt.template. prompt.toml avoids both ambiguity and warning noise.

The prompt-specific overrides are SHOAL_PROMPT_LEFT, SHOAL_PROMPT_RIGHT, SHOAL_PROMPT_THEME, and SHOAL_NERD_FONT. For the font variable, 1 means always and 0 means never.

Format placeholders🔗

A format string combines literal text with $module_id placeholders. The fixed IDs are:

character directory git_branch git_status git_state cmd_duration
exit_status jobs time username hostname reef principal leash battery

$indent is also recognized. Dynamic modules use $language_NAME and $custom_NAME, where NAME is the table key under [module.language] or [module.custom].

[format]
left = "$directory$git_branch$language_rust$custom_cluster$character"

[module.language.rust]
tool = "rustc"
symbol = "rs "
style = "red"
when = "constrained"
probe_ttl_s = 30
format = "${symbol}${version} "

[module.custom.cluster]
command = "kubectl config current-context"
when = "KUBECONFIG"
cache_ttl = "10s"
style = "blue bold"
format = "${output} "

These dynamic tables have a sharp type boundary: language.when, custom.command, custom.when, and custom.cache_ttl are strings, not arrays or numeric durations. A wrong type makes the rich prompt fail deserialization and fall back to the complete default prompt, with a warning.

The schema is ahead of the host wiring here. Language modules can render an existing Reef binding; only the exact value when = "constrained" has distinct behavior, while every other string currently uses the same “resolved or constrained” test. probe_ttl_s is not consumed by the host. The host currently supplies no custom-command snapshots and no battery snapshot, so $custom_NAME and $battery render empty; custom.command, custom.when, and custom.cache_ttl describe planned scheduling rather than an operational background runner. Keep those tables out of a production prompt unless you are testing renderer input directly.

Unknown format module IDs and unknown prompt keys warn rather than preventing the shell from starting. Invalid prompt data falls back to defaults with a warning.

Module configuration map🔗

Each static table supports enabled plus module-specific keys:

TableImportant keys
module.charactersuccess_symbol, error_symbol, vicmd_symbol, and their styles
module.directorytruncate_to, truncate_style, repo_relative, read_only_symbol, symbol, style, home_symbol
module.git_branchsymbol, ascii_symbol, style, truncate_to, truncate_symbol, format
module.git_statusahead, behind, diverged, staged, unstaged, untracked, conflicted, stashed, stale_symbol, engine, style
module.git_statelabels for rebase, merge, cherry_pick, bisect, revert, plus style
module.cmd_durationmin_ms, style
module.exit_statusshow_on_success, format, style
module.jobssymbol, threshold, format, style
module.timeformat, style
module.usernameshow_always, style, root_style
module.hostnameshow_always, symbol, style
module.reefformat, show_when_empty, show_ambient, style
module.principalhuman_symbol, agent_symbol, show_agent_name, style, agent_style
module.leashstyle_by_tier, symbol_by_tier, hide_when_enforced
module.batterycharging/discharging symbols, low_threshold, low_style, style, sample_interval_s

Style strings accept color names and attributes such as bold and dim. A style can also name a palette entry (ok, error, warn, info, muted, or accent).

Prompt performance model🔗

Prompt rendering is snapshot-based. Shoal collects mutable facts once after a command completes and the renderer formats that immutable snapshot while the line editor redraws. A redraw therefore does not repeatedly inspect the filesystem or launch commands. In a Git repository, status collection runs at most one git status subprocess per completed command.

Inspect or benchmark the result without opening an interactive shell:

shoal prompt print --side left
shoal prompt explain --side right
shoal prompt bench --side left --n 1000

If the prompt exceeds budget.render_deadline_ms, Shoal can warn when warn_on_exceed is enabled. Static/Git/Reef snapshot work should stay cheap. The custom-command/cache configuration is future-shaped and inert in the current host because no custom snapshot scheduler populates it; do not rely on custom.command, custom.when, or custom.cache_ttl executing anything yet.

Troubleshooting configuration🔗

  • If a repository .shoal.toml seems ignored, remember that main configuration uses the nearest ancestor but prompt configuration only uses the file in the exact startup directory.
  • If rich prompt keys warn from shoal.toml, move them to the root of prompt.toml.
  • If a legacy {cwd} template appears, migrate it to format.left = "$directory...".
  • If output is unexpectedly silent in a script, set render.echo = "all" temporarily; see CLI and execution modes.
  • If a configured adapter disappears, consolidate the complete catalog into the last adapters.dirs entry.
  • If a schema-valid setting has no effect, check the runtime-note column above and Current status and limits.
Type to search every guide navigate open esc close
Diagram