Skip to content

Audit System

lash includes a tamper-evident audit system for logging hook events. The audit log is a single append-only JSONL file at ~/.lash/audit.log, protected by a secret-ratcheted hash chain. Implemented as a built-in plugin.

salt = random 16-byte hex
secret_0 = KDF(password, salt) -- 600,000 iterations HMAC-SHA256
hash_1 = HMAC-SHA256(secret_0, content_1)
secret_1 = HMAC-SHA256(secret_0, hash_1) -- ratchet forward
hash_2 = HMAC-SHA256(secret_1, content_2)
secret_2 = HMAC-SHA256(secret_1, hash_2)
...

Each secret ratchets forward — only the current value exists. Previous secrets are destroyed.

ScenarioOutcome
Attacker has log onlyCannot forge any hash
Attacker has key file onlyCan forge future, not past
Attacker has bothCan forge future, past is locked
User verifies with passwordFull chain validated

The password is never stored. A verification hash (derived from password + ":verify") is stored for password-gated profile escalation.

~/.lash/audit.key (permissions 0600):

<salt>:<64-char hex secret>:<entry count>:<password verify hash>

Updated atomically (write .tmp, then rename()). On startup, entry count is compared against actual log lines; a mismatch indicates tampering.

{
"action": "pipeline.pre_execute",
"ts": "2026-03-06T14:30:00.123",
"seq": "42",
"sid": "s_123",
"command": "ls -la",
"cwd": "/home/user",
"hash": "a3f2...c9d8"
}
FieldDescription
actionHook event name
tsTimestamp (ISO 8601 extended)
seqSequence number (monotonically increasing)
sidSession ID
hashHMAC-SHA256 hash from the ratcheted secret chain

The audit plugin observes: session.connect, session.disconnect, pipeline.pre_execute, pipeline.post_execute, variable.set, variable.drop, job.background, job.foreground, error.dispatch.

The audit plugin registers audit and profile as plugin methods. All audit commands require the auditAccess capability, which the plugin checks internally. Passwords are collected via prompt_password (see Plugin System) — never via CLI args or env vars.

CommandDescription
audit initInitialize audit system. Generates salt, derives secret, writes key file. Prompts you to select a default profile.
audit verifyRecomputes full ratchet chain, verifies every entry. Exit 0 if intact, 1 if tampered.
audit rotateVerifies log, deletes if valid, resets chain with new salt.
profilePrint current session profile (no password needed).
profile <name>Switch session profile (requires audit password).
Audit Report
===================================
Entries: 1247
Period: 2026-01-15T08:00:00 -> 2026-03-06T14:30:00
Status: INTACT
Events by type:
pipeline.post_execute: 489
pipeline.pre_execute: 489
session.connect: 120
session.disconnect: 118
Violations: 0

On startup, the audit plugin warns via stderr when thresholds are exceeded:

ConditionDefault Threshold
File size50 MB
File age7 days
Entry count mismatch(always)

Configure thresholds in ~/.lash/config:

[settings]
audit.max_size_mb = 50
audit.max_age_days = 7
Audit LogSession Logs
PurposeTamper-evident security trailCommand history/replay
ScopeAll sessions, all hook eventsOne session’s commands
IntegritySecret-ratcheted hash chainPlain JSONL
AudienceSysadmins, complianceThe user
Location~/.lash/audit.log~/.lash/sessions/*.jsonl