Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

Fish Startup Files: Execution Order, Scope Gates, and Configuration Diagnostics

Map Fish startup order across user, system, and vendor snippets; gate login and interactive work, diagnose collisions, and keep shell startup predictable.

Fish startup behavior is easiest to reason about as an ordered program, not as one configuration file that runs only when a terminal window appears. Fish reads configuration snippets from several user, system, and vendor locations, then reads its system-wide and user configuration files. Those files run for each Fish process, including noninteractive shells. A command placed at top level may therefore run in contexts where a user expected only prompt initialization.

The practical questions are: which file ran, in what order, under what shell mode, and how many times? Answer those before moving lines between files or adding another startup hook.

The startup sequence

Fish reads configuration snippets named *.fish from $__fish_config_dir/conf.d, $__fish_sysconf_dir/conf.d, directories under $__fish_user_data_dir, and fish/vendor_conf.d directories under XDG_DATA_DIRS. It then reads the system-wide config.fish, followed by the user’s config.fish. The user configuration is deliberately later than the snippets so that personal settings can adjust behavior established by snippets.

Within the snippet search, Fish executes files in natural filename order, and if the same filename is present in multiple directories only the first is executed. This means a duplicate name is not an overlay or merge. It is a precedence decision. Prefixing snippets with numbers can make ordering legible, but a filename collision across directories can still mean that one copy never runs.

# Example paths. Exact system and vendor locations vary by installation.
~/.config/fish/conf.d/10-path.fish
~/.config/fish/conf.d/40-prompt.fish
~/.config/fish/config.fish
/etc/fish/conf.d/20-system.fish
/etc/fish/config.fish

The order shown as a directory tree is not a literal execution sequence; it groups paths for readability. The user, system, and vendor snippet directories are processed according to Fish’s configured search paths, and the main system and user files follow the snippets. Use the running shell’s path variables and official configuration documentation rather than assuming every operating system installs Fish under the same prefix.

Natural ordering is useful for predictable initialization, but it is not a dependency manager. If one snippet requires a function or variable established by another, make the ordering and ownership explicit and keep the coupling small. Better still, combine tightly related setup into one file or make a function validate its prerequisites instead of relying on an accidental alphabetic relationship.

Gate work by shell mode

Every startup configuration file is executed for every new Fish shell. Test status –is-interactive before prompt, keybinding, or terminal UI setup. Use status –is-login only for work that belongs to login-session initialization. A shell may be interactive without being a login shell, or a login shell without being interactive; these are separate properties.

# This work belongs only in an interactive shell.
if status --is-interactive
    set -g fish_greeting ''
    # Initialize interactive-only functions or display settings here.
end

# This work belongs only in login sessions.
if status --is-login
    # Login-session setup goes here.
end

Use explicit condition blocks instead of treating every new Fish process as a terminal. A script can invoke Fish in a noninteractive context, and another shell can launch Fish only to evaluate a command. Expensive external commands, terminal control sequences, and user-facing output at top level can turn those otherwise quiet invocations into slow or noisy operations.

Do not assume Fish has the same login/interactive startup split as Bash. Ported dotfiles often retain the source shell’s mental model and place logic in a file Fish does not read. Start from Fish’s documented sequence and use the shell’s status predicates to express when a piece of configuration is valid.

Understand conf.d precedence and vendor integration

The conf.d mechanism lets packages and administrators install focused initialization snippets without modifying a user’s config.fish. Fish also exposes variables describing its configuration paths. The directories and vendor paths are affected by build configuration and environment, so do not hard-code common Unix prefixes as universal facts.

When duplicate snippet names exist, only the first is executed. Do not create a same-named file in the user directory as an assumed override unless the relevant path ordering has been verified. Prefer a distinct user filename and use the later user config.fish to adjust behavior where appropriate. If a package snippet must be disabled, use that package’s supported configuration rather than relying on a filename collision whose result can change with installation paths.

# Inspect the current shell's configuration roots.
printf 'user config: %s\n' $__fish_config_dir
printf 'system config: %s\n' $__fish_sysconf_dir
printf 'vendor snippets:\n'
printf '  %s\n' $__fish_vendor_confdirs

Internal variables beginning with __fish are implementation-oriented. Read them for diagnosis, but do not generally set them as a supported customization interface. A package should discover its installation paths according to its platform packaging and Fish’s documented integration mechanism.

Trace what actually ran

When a setting is missing, identify the configuration file and execution point instead of appending another copy of the setting. Fish provides status current-filename and status current-line-number for tracing the current file and line. A temporary trace can write to stderr so it does not contaminate stdout used by a script.

if set -q FISH_STARTUP_TRACE
    printf 'fish config: %s:%s\n' \
        (status current-filename) \
        (status current-line-number) >&2
end

Place this trace temporarily at the top of suspected files or wrap a narrow section with a marker. Remove it after diagnosis. For larger startup problems, measure a normal shell and compare against a minimal test account or a carefully isolated configuration directory. Preserve the user’s normal files while testing; do not rename or overwrite live configuration without a backup.

Inspect both the files and the effective runtime state. A setting can be assigned and later replaced; a function can be redefined by a later source; a path can be prepended in several snippets. The location of a declaration does not prove that its value is still active at the prompt. Check the final variable value, function definition, and binding after startup.

Keep startup work cheap and idempotent

Startup code runs frequently, so avoid doing repeated work that can be moved to an explicit command. A completion or function can often be loaded on demand rather than initialized by launching external programs in every shell. Do not query a network service, refresh a large cache, or block on user input from a top-level configuration file.

Idempotence matters when a configuration file is sourced manually during debugging. Appending the same path repeatedly, spawning duplicate background helpers, or adding duplicate event handlers can create a problem that is absent in a clean shell but grows during troubleshooting. Prefer declarative settings and check-before-add logic when repeated sourcing is a supported workflow.

Avoid output at startup unless it is intentional user-interface output. A banner or debug message written to stdout can break a script that expects a command’s output. Even stderr can surprise build systems or test harnesses. Gate diagnostics behind a variable or an interactive check and direct temporary trace output to stderr.

Common symptoms and their likely layer

If a setting applies in a terminal but not in a script, check whether it is guarded by status –is-interactive or whether the script launches Fish with a different environment. If it applies in one terminal but not another, compare login status, XDG configuration paths, vendor paths, and shell version. If it works only after manually sourcing a file, check whether the file is in a discovered path and whether an earlier duplicate basename suppresses it.

If a startup change seems ignored, inspect later snippets and main config files for a second assignment. If a new terminal is slow, measure external commands in the configuration and gate terminal-only work. If a noninteractive Fish command prints prompt-related output, locate unguarded top-level statements. A configuration file is executable code, so treat startup as a dependency graph whose side effects should be observable and bounded.

A disciplined change and rollback procedure

  1. Record the active user, system, and vendor configuration roots.
  2. Find duplicate snippet basenames and check which copy is selected.
  3. Trace the configuration file and line that assigns the setting.
  4. Determine whether the desired behavior is interactive-only, login-only, or universal.
  5. Make one change in the narrowest appropriate file.
  6. Start a fresh Fish process and validate the effective state in both target modes.
  7. Measure startup time and ensure stdout remains clean for noninteractive invocations.
  8. Keep a reversible diff until the configuration has been exercised by relevant terminals and scripts.

This workflow is safer than copying more initialization into config.fish. Duplicated setup obscures precedence and makes rollback harder. The goal is a single clear owner for each setting, with the startup mode and execution order made explicit.

Related:

Sources:

Comments