Skip to content

File accessUser activityExecution

viminfo, ShaDa and lesshst: Linux Editor and Pager History

Vim viminfo, Neovim ShaDa and less history files on Linux: which files a user edited, what they searched for and typed, often after shell history is wiped.

Location
~/.viminfo
Proves
Which files a user opened in vim or Neovim, and what they searched for or ran inside vim and less
Timestamps
Unix epoch seconds in viminfo bar lines and ShaDa entries; none in lesshst
Access
Any user for own files (created mode 0600); root for other users
Retention
Until deleted; bounded by the 'viminfo'/'shada' limits and LESSHISTSIZE (default 100)
Collection
UAC, Velociraptor, cp -a, tar

What it is

Vim, Neovim and less save state between sessions in small per-user files. Vim writes ~/.viminfo (plain text), Neovim writes a ShaDa ("shared data") file in MessagePack, and less writes a history file of searches and shell escapes. Attackers who clear ~/.bash_history often forget these, and the vim files in particular record full file paths with timestamps.

Several other command-line tools keep similar per-user histories, listed below. They are small, easy to collect together, and useful for the same reasons.

Where it lives

ToolDefault pathNotes
Vim~/.viminfoOther name via the n flag in 'viminfo', -i file or 'viminfofile'; -i NONE disables it
Neovim 0.8+~/.local/state/nvim/shada/main.shada$XDG_STATE_HOME/nvim/shada/; earlier Neovim used ~/.local/share/nvim/shada/main.shada
less (598 and later)First existing of $XDG_STATE_HOME/lesshst, ~/.local/state/lesshst, $XDG_DATA_HOME/lesshst, ~/.lesshstLESSHISTFILE overrides; - or /dev/null disables. less 590 to 597 used $XDG_DATA_HOME/lesshst only when that variable was set; older less uses ~/.lesshst
Python REPL (3.4+)~/.python_historyWritten by the interactive interpreter's readline hook
psql~/.psql_historyPSQL_HISTORY or the psql HISTFILE variable override it; HISTSIZE default 500
mysql client~/.mysql_historyMYSQL_HISTFILE overrides; lines matching *IDENTIFIED*:*PASSWORD* are not logged by default
wget~/.wget-hstsHosts that sent wget an HSTS header: <hostname> [<port>] <include subdomains> <created> <max-age> per line
sqlite3 shell~/.sqlite_historyCollected by UAC

Paths are identical across Debian/Ubuntu, RHEL/Fedora and other families. Where ~/.local/state exists (common on current desktops), newer less writes there rather than to ~/.lesshst, so check both.

What it proves

  • viminfo/ShaDa file marks and jumplist: that the account opened a given file in vim or Neovim, the cursor line when it left, and (in bar lines or ShaDa) when.
  • Command-line history: ex commands such as :w /tmp/out, :%s/old/new/g or :!id, which can show edits and shell commands run from inside the editor.
  • Search history and registers: strings searched for and text yanked or deleted, sometimes including pasted secrets or code.
  • lesshst: search patterns and shell or pipe commands (!cmd, |) run from less, plus marks with file names when --save-marks is used.
  • It does not prove the file was saved or changed; opening read-only still records marks. It does not record every file opened, only those with marks written at exit.

Key fields

viminfo

Section / lineMeaning
# This viminfo file was generated by Vim 9.1.Writer version
# Command Line History (newest to oldest):Ex commands, one per : line
# Search String History (newest to oldest):Search patterns (lines starting with ?)
# Registers:Register contents ("a, "0...)
# File marks:'0 to '9 (last exit positions) and 'A-'Z, with line, column and path
# Jumplist (newest first):Positions jumped to, per file
# History of marks within files (newest to oldest):One > /path/to/file block per file with its local marks
|2,<type>,<timestamp>,...Bar line: history entry with epoch time
|3,... / |4,<mark>,<line>,<col>,<timestamp>,"path"Bar lines for registers / marks with epoch time

ShaDa

Each entry is a sequence of MessagePack values: entry type, timestamp, length, then the data. Types include 4 history, 5 register, 7 global mark, 8 jump, 9 buffer list, 10 local mark and 11 change.

lesshst

First line .less-history-file:, then sections .search, .shell and .mark. Entries in the search and shell sections are prefixed with a double quote.

Timestamps

  • Vim bar lines (Vim 8.0 and later) and ShaDa entries store Unix epoch seconds (UTC) of when the item was set.
  • lesshst, .python_history, .psql_history and .mysql_history contain no times; use the file mtime, which reflects the last write at exit.
grep -E '^\|4,' ~/.viminfo | while IFS=, read -r _ m l c ts f; do
  printf '%s mark=%s line=%s %s\n' "$(date -u -d @"$ts" +%FT%TZ)" "$m" "$l" "$f"; done

Retention

  • Vim limits what it keeps through the 'viminfo' option; the Unix default '100,<50,s10,h keeps marks for 100 files, registers up to 50 lines and items up to 10 KiB, while history lengths follow the 'history' option. The file is merged and rewritten on each clean exit, keeping the newest items by timestamp across concurrent sessions.
  • Neovim applies the equivalent 'shada' option and merges by timestamp.
  • less keeps LESSHISTSIZE entries (default 100) per list.
  • Nothing is written when vim is killed. Failed writes can leave temporary ~/.viminf* files behind, which are worth collecting.

Collection

UAC's ir_triage profile includes files/applications/viminfo.yaml, lesshst.yaml (all three less locations), wget.yaml, and full adds python.yaml and sqlite.yaml. Collect /root too: $HOME decides which viminfo vim uses, so after su or sudo the entries can land in either home.

cd /mnt/evidence
find root home -maxdepth 6 \( -name '.viminfo' -o -name 'main.shada' -o -name '*lesshst' \
  -o -name '.python_history' -o -name '.psql_history' -o -name '.mysql_history' \
  -o -name '.wget-hsts' -o -name '.sqlite_history' \) -print0 | tar --null -T - -czf edhist.tgz

Include database service homes (/var/lib/postgresql, /var/lib/mysql) where client histories often live.

Parsing

  • Plaso has a text/viminfo parser plugin for timeline events from viminfo.
  • Neovim ships a ShaDa plugin that shows a .shada file decoded when you open it in Neovim (open a copy, not the original).
  • Python: read ShaDa with the msgpack package.
import msgpack, datetime
with open("main.shada", "rb") as f:
    u = msgpack.Unpacker(f, raw=False, strict_map_key=False)
    while True:
        try:
            etype, ts, _len, data = next(u), next(u), next(u), next(u)
        except StopIteration:
            break
        print(etype, datetime.datetime.fromtimestamp(ts, datetime.UTC).isoformat(), data)

Investigator tips

  • The '0 file mark is the file and line where vim last exited, a quick answer to "what was the last thing edited".
  • Command-line history entries like :!, :r ! or :w !sudo tee % show shell execution and privilege use from inside the editor that never touches ~/.bash_history.
  • Editing /etc/shadow, /etc/sudoers, authorized_keys, cron files or web roots in vim leaves paths in viminfo; confirm with inode times (see ext4 timestamps).
  • Vim refuses to use a viminfo that is a symbolic link, so a .viminfo pointing to /dev/null is an unusual, deliberate change; -i NONE or set viminfo= in a vimrc also disables it.
  • A mismatch between a clean ~/.bash_history and a rich viminfo or lesshst suggests selective cleanup.
  • For GUI editors, look at recently-used.xbel instead.

See also