Fish History Persistence: Session Names, Merging, Search, and Recovery
Trace Fish history storage, session names, merging, search and deletion semantics, private mode, and practical recovery steps across concurrent shells.
Fish history is a persistent record of interactive commands, but it is not a simple append-only text log shared identically by every open shell at every moment. Fish tracks history in sessions, keeps a current in-memory view, and provides explicit operations for searching, merging, clearing, and saving. Understanding these boundaries explains why a command entered in one terminal may not immediately appear in another, why a history search result is not always a durable record, and why a custom session name can make a history file seem to disappear.
History also has privacy behavior that differs from ordinary shell variables. With the default history policy, a command prefixed with a space is omitted from persistent history, and Fish private mode suppresses writing to disk. A custom fish_should_add_to_history function takes over the add/omit decision, so leading-space behavior can differ. These mechanisms are useful, but they should be understood as shell-history behavior rather than a general privacy guarantee for terminal logs, process accounting, or external command output.
Locate the store before changing configuration
By default, Fish stores command history under ~/.local/share/fish/fish_history. When XDG_DATA_HOME is set, the path is under that data directory instead. The fish_history environment variable changes the history session name, producing a corresponding session history file; it is not a filesystem path setting. A non-default session name is therefore a common explanation for commands being written to a different file than expected.
# Inspect the configured session name.
set -q fish_history; and printf 'session=%s\n' $fish_history
# Inspect the effective XDG data directory.
set -q XDG_DATA_HOME; and printf 'data-home=%s\n' $XDG_DATA_HOME
# Search the current Fish history view.
history search --contains deploy --max 20
The configuration command above is illustrative; a missing fish_history value can mean Fish is using its default session, not that history has failed. When diagnosing a mismatch, inspect the running shell’s environment and the expected on-disk directory before editing variables. Also check whether a shell was started with a distinct session name by a terminal profile, a project tool, or a wrapper.
Do not set fish_history to an absolute file path. The documented use is a session name that identifies a separate history stream. If the goal is to separate work and personal commands, choose a clear session name and configure it consistently for the relevant shells. If the goal is to share one history context, use the same session rather than copying files while Fish is running.
Concurrent shells and the merge boundary
Each Fish process has a view of history and ordinarily ignores history changes from sessions that were started after that shell began. The history merge command immediately incorporates changes made by other sessions. This explicit merge behavior avoids assuming that all terminals synchronously refresh every other terminal’s interactive state.
# Bring changes from other sessions into this session's view.
history merge
# Search the now-merged view for a recent operation.
history search --contains 'deployctl apply' --max 20
If a command appears in one terminal but not another, first compare the sessions and then run history merge in the older shell. Do not immediately concatenate history files or delete lock-like files by hand. Fish’s builtin knows how to incorporate the history state; filesystem edits made while shells are active can create confusing or conflicting observations.
Merging does not imply a real-time notification stream. A long-lived shell can have a stale view until the merge occurs, while a newly launched shell reads the relevant session data during startup. Build operational habits around the documented merge command when you need to search across active sessions. For scripts that need a durable audit trail, write structured records to a dedicated logging system rather than treating interactive history as the source of truth.
Search semantics and machine processing
The history command searches by contains by default. It can also search for a prefix or an exact entry, limit the number of matches, reverse result order, select case sensitivity, and include timestamps. Search results normally appear newest first. If a command contains embedded newlines, use null-terminated output with –null when piping results into a consumer that supports NUL-delimited records.
# Exact, case-sensitive lookup.
history search --exact --case-sensitive 'git status'
# Oldest-first search results for a small set.
history search --contains 'release' --reverse --max 50
# Multiline-safe output for a NUL-aware reader.
history search --contains 'function' --null | while read -z entry
printf 'entry: %s\n' $entry
end
The final example demonstrates NUL-aware input, but downstream code must preserve the resulting entries. Do not process history by splitting on ordinary newlines if multiline commands are possible. Do not assume the displayed numeric IDs are permanent identifiers; they are useful for the interactive delete prompt, not an externally stable database key.
Search can be case-insensitive by default; case sensitivity is explicitly selectable. Exact matching without case sensitivity may still treat case variants as equivalent, so add –case-sensitive when that distinction matters. Test matching mode on a short sample before deleting entries or generating an export.
Delete narrowly and understand the confirmation layer
The history function can prompt before deleting matching entries. By default, the function uses interactive confirmation for deletion, where a user selects one or more displayed entries or cancels. The lower-level history builtin supports exact, case-sensitive deletion only; do not conflate the interactive function’s richer search prompt with the builtin’s deletion interface.
# Interactive deletion of entries containing an exact phrase.
history delete --contains 'temporary-token'
# Exact and case-sensitive deletion of one exact command.
history delete --exact --case-sensitive 'git status'
Review the target set before confirming a broad deletion. Contains searches can match more than one command, and case-insensitive matching can include differently capitalized entries. If you need a narrow cleanup, start with history search using the same matching mode and inspect the results. Avoid automating history deletion against broad patterns without a tested list of intended matches.
history clear removes the session’s history and normally asks for confirmation. history clear-session is different: it clears activity associated with the current session, and after a merge only the history after that merge is cleared. If the issue is one unwanted command, use targeted deletion rather than clearing an entire session.
Persistence is automatic; save is usually not the fix
Fish automatically saves history changes. The history save operation writes accumulated changes immediately, but the documentation says ordinary users normally should not need to call it. Repeatedly invoking save as a workaround can mask the real problem: a different session name, a different XDG data directory, a shell running in private mode, or a path ownership/permission issue.
# Inspect recent entries in the current session.
history search --max 10 --show-time
# Ask Fish to write pending history changes immediately if an operational
# workflow specifically requires it.
history save
Before changing permissions or relocating files, verify the path Fish is using. If the directory belongs to a different account, a container, or a synchronized home directory, determine that environment boundary before attempting repairs. Do not copy a live history file over itself or replace it while multiple Fish processes may be updating the session.
History is intended for interactive convenience. It can omit entries intentionally, and private mode can prevent disk writes. It is not a reliable record of every command run by scripts or noninteractive processes. Use system auditing, job logs, or application-level event records when completeness, retention, or tamper evidence is required.
Private mode and intentional omission
By default, prefixing a command line with a space prevents that full line from being stored on disk. The line remains available for recall until the next command is executed. A custom fish_should_add_to_history function replaces this default policy and decides whether space-prefixed commands are kept. Fish also supports private mode, which prevents history from being written to the file. Launching Fish with --private both hides old history and prevents new writes; setting fish_private_mode to a non-empty value enables private behavior without claiming that all other terminal or operating-system traces are hidden.
# Start a shell that hides prior history and does not write new history.
fish --private
# A Fish script can respect the user's current private-mode setting.
if test -n "$fish_private_mode"
printf 'History persistence is disabled in this shell.\n' >&2
end
Private mode is scoped to Fish history. It cannot erase command data already recorded elsewhere, and it cannot prevent child programs or terminal software from logging their own activity. Do not promise confidentiality based only on a leading space or private shell. Use the appropriate data-handling controls for the system and application involved.
A recovery runbook for missing entries
When history appears not to persist, diagnose it in this order:
- Confirm that the missing command was entered at an interactive Fish prompt and was not intentionally omitted with a leading space.
- Check whether Fish private mode is active.
- Inspect the current fish_history session name in both the writing shell and the shell doing the search.
- Check XDG_DATA_HOME and the resulting history directory in each environment.
- Run history merge in a long-lived shell, then search again.
- Inspect whether a newly started shell is reading a different session file.
- Only after confirming the target path, inspect filesystem ownership, permissions, and available storage.
Keep evidence before changing configuration: record the process environment, session name, directory, and relevant timestamps. If the file is shared or synchronized, check whether another process or host is participating. Avoid destructive cleanup until you know which session is the intended one.
Acceptance checks for a history setup
A useful setup has a documented session strategy, a known XDG data root, and an understood merge workflow for long-lived terminals. Validate a fresh shell, two simultaneous sessions, a merge, exact and contains search, an interactive deletion test on disposable commands, and private mode in a separate test process. Confirm that changing a session name creates the intended separate stream without overwriting the default one.
If you automate history processing, test multiline entries and use NUL delimiters where required. If your operational requirement is a complete audit trail, make that requirement explicit and use a proper logging system. Fish history improves interactive recall; it does not promise durable, cross-process audit semantics.
Related:
- History Expansion and Search: How !!, Ctrl-R, and Shell History Files Actually Work
- Fixing Shell History That Doesn’t Persist or Save Correctly
Sources: