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
Tools
Compare all toolsWhat 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
| Tool | Default path | Notes |
|---|---|---|
| Vim | ~/.viminfo | Other 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, ~/.lesshst | LESSHISTFILE 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_history | Written by the interactive interpreter's readline hook |
| psql | ~/.psql_history | PSQL_HISTORY or the psql HISTFILE variable override it; HISTSIZE default 500 |
| mysql client | ~/.mysql_history | MYSQL_HISTFILE overrides; lines matching *IDENTIFIED*:*PASSWORD* are not logged by default |
| wget | ~/.wget-hsts | Hosts that sent wget an HSTS header: <hostname> [<port>] <include subdomains> <created> <max-age> per line |
| sqlite3 shell | ~/.sqlite_history | Collected 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/gor:!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 fromless, plus marks with file names when--save-marksis 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 / line | Meaning |
|---|---|
# 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_historyand.mysql_historycontain no times; use the filemtime, 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,hkeeps 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
LESSHISTSIZEentries (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/viminfoparser plugin for timeline events from viminfo. - Neovim ships a ShaDa plugin that shows a
.shadafile decoded when you open it in Neovim (open a copy, not the original). - Python: read ShaDa with the
msgpackpackage.
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
'0file 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
.viminfopointing to/dev/nullis an unusual, deliberate change;-i NONEorset viminfo=in a vimrc also disables it. - A mismatch between a clean
~/.bash_historyand a rich viminfo or lesshst suggests selective cleanup. - For GUI editors, look at recently-used.xbel instead.