How Suvadu Records Shell History with <2ms Overhead
When you're building a tool that hooks into every single command execution, performance isn't a feature. It's a hard requirement. If your shell feels even slightly slower, you've failed. Suvadu captures every command with full metadata in under 2 milliseconds. Here's exactly how.
The Architecture
Suvadu's recording pipeline has three stages, all of which happen between the time you press Enter and the time your next prompt appears:
- Preexec hook (before command runs): Capture the command string and record start time
- Command executes: Suvadu is not involved here at all
- Precmd hook (before next prompt): Capture exit code, compute duration, detect executor, write to SQLite
The overhead you feel is only in steps 1 and 3. Step 1 is essentially free (under 0.1ms). Step 3 is where the real work happens, and it consistently completes in 1.5-2ms.
Stage 1: The Preexec Hook
Zsh provides a preexec function that fires just before any command runs. Here's what Suvadu's hook does:
_suvadu_preexec() {
_SUVADU_CMD="$1"
_SUVADU_START_TIME=$(( ${EPOCHREALTIME%.*} * 1000 + ${EPOCHREALTIME#*.}[:3] ))
}Two variable assignments. That's it.
The key insight is using Zsh's native $EPOCHREALTIME variable, available since Zsh 5.1 (2015). It gives you the current Unix timestamp with microsecond precision as a float (e.g., 1707500000.123456). We extract millisecond precision using pure Zsh string manipulation and arithmetic — no subprocesses at all.
Stage 2: Executor Detection
Before writing to the database, Suvadu needs to know who ran the command. The detection logic is a cascade of environment variable checks:
# Simplified from actual hook code
detect_executor() {
# 1. CI/CD environments
if [[ -n "$CI" ]]; then
# Check GITHUB_ACTIONS, GITLAB_CI, CIRCLECI...
# 2. AI agents
elif [[ -n "$CLAUDE_CODE" ]]; then
executor_type="agent"; executor="claude-code"
elif [[ -n "$CODEX_CLI" ]]; then
executor_type="agent"; executor="codex-cli"
# 3. IDE terminals
elif [[ -n "$CURSOR_INJECTION" ]]; then
executor_type="ide"; executor="cursor"
elif [[ -n "$VSCODE_INJECTION" ]]; then
executor_type="ide"; executor="vscode"
# 4. Human (TTY check)
elif [[ -t 0 ]]; then
executor_type="human"; executor="terminal"
# 5. Fallback
else
executor_type="programmatic"; executor="subprocess"
fi
}Environment variable checks in Zsh are essentially free (nanosecond-scale lookups against the process environment block). The [[ -t 0 ]] TTY check is a single isatty() syscall. The entire detection cascade completes in under 0.5ms.
This detection happens per-command because the executor context can change during a session. For example, Claude Code might spawn a shell, run some commands (detected as agent/claude-code), and then you type a command manually (detected as human/terminal), all within the same shell session.
Stage 3: The SQLite Write
The precmd hook calls suv add with all the captured metadata:
suv add \
--session-id "$SUVADU_SESSION_ID" \
--command "$_SUVADU_CMD" \
--cwd "$PWD" \
--exit-code "$exit_code" \
--started-at "$_SUVADU_START_TIME" \
--ended-at "$end_time" \
--executor-type "$executor_type" \
--executor "$executor"This is the most expensive operation in the pipeline. It involves:
- Spawning the
suvbinary (~0.5ms on a warm filesystem cache) - Opening the SQLite database connection (~0.2ms)
- Executing a single parameterized INSERT (~0.5ms)
- Process exit and cleanup (~0.3ms)
Why WAL Mode Matters
SQLite's default journal mode uses rollback journaling, which requires an exclusive lock on the entire database for every write. This means a concurrent read (like a search query in another terminal) would block the write, or vice versa.
WAL (Write-Ahead Logging) mode changes this fundamentally:
// From db.rs - connection initialization conn.pragma_update(None, "journal_mode", "WAL")?; conn.pragma_update(None, "synchronous", "NORMAL")?;
With WAL mode, writes go to a separate .db-wal file while readers continue accessing the main database file. There's no lock contention between the recording hook and any concurrent search operations.
The synchronous=NORMAL pragma is equally important. The default FULL mode calls fsync() after every transaction to guarantee durability, which can add 5-20ms depending on the storage device. NORMAL only syncs at checkpoint boundaries, which makes individual inserts dramatically faster while still being safe against application crashes (you might lose the last transaction on a power failure, but not on a normal crash or kill).
The Insert Query
The actual SQL that records each command:
INSERT INTO entries (
session_id, command, cwd, exit_code,
started_at, ended_at, duration_ms,
context, tag_id, executor_type, executor
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)It's a single parameterized INSERT with no subqueries, no joins, no computed columns. The tag_id is resolved beforehand via auto-tag configuration (a prefix match against the current working directory). The duration_ms is computed in the hook as ended_at - started_at.
The Database Schema
Suvadu's schema is designed for fast writes and flexible reads:
-- Core table: one row per command execution
CREATE TABLE entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
command TEXT NOT NULL,
cwd TEXT NOT NULL,
exit_code INTEGER,
started_at INTEGER NOT NULL, -- unix milliseconds
ended_at INTEGER NOT NULL,
duration_ms INTEGER NOT NULL,
context TEXT, -- reserved for future use
tag_id INTEGER REFERENCES tags(id),
executor_type TEXT,
executor TEXT
);
-- Indexes optimized for common query patterns
CREATE INDEX idx_entries_started_at ON entries(started_at); -- time range
CREATE INDEX idx_entries_command ON entries(command); -- search
CREATE INDEX idx_entries_session_id ON entries(session_id); -- session lookup
CREATE INDEX idx_entries_tag_id ON entries(tag_id); -- tag filteringThe started_at index is the most critical. Since the TUI always sorts by recency (ORDER BY e.started_at DESC), this index means the database can walk the B-tree in reverse order without scanning the table.
The command index accelerates LIKE '%query%' searches. While a B-tree index can't speed up arbitrary substring searches (the leading % prevents index prefix matching), SQLite can still use it for index-only scans when the query planner determines it's more efficient than a full table scan.
Synchronous by Design
One deliberate design choice: the suv add call is synchronous. It blocks the shell until the write completes. This might seem counterintuitive for a tool obsessed with speed, but there's a good reason.
If the write were async (backgrounded with &), pressing the up arrow immediately after a command would sometimes miss the most recent entry because the write hadn't completed yet. For a history tool, that's unacceptable. The 1.5ms of synchronous blocking is worth the guarantee that your last command is always immediately searchable.
The Search Side
While recording needs to be fast, search needs to feel instant. Suvadu's TUI uses ratatui (a Rust TUI framework) and renders on every keystroke. The query path looks like:
- User types in the search box
- A parameterized SQL query fires with
LIKE '%input%'plus any active filters (date range, tag, exit code, executor, directory) - Results are paginated with
LIMIT/OFFSET - The TUI renders the result table, detail pane, and status bar
With 100,000 entries, a filtered search query returns in 2-5ms. With 500,000 entries, it's still under 10ms. SQLite's query planner is remarkably good at combining multiple indexes when you have compound WHERE clauses.
Arrow Key Frecency
The up/down arrow key integration uses a different query path optimized for prefix matching:
SELECT command FROM entries WHERE command LIKE 'git comm%' -- prefix of current input AND cwd = '/Users/me/project' -- same directory boost ORDER BY started_at DESC LIMIT 1 OFFSET 3 -- 4th most recent match
The prefix match (LIKE 'query%' without a leading wildcard) can use the B-tree index on command, making it significantly faster than the TUI's substring search. Combined with the recency sort, this gives you frecency-ranked navigation that feels native.
Putting It All Together
Here's the complete timing breakdown for recording a single command:
| Stage | Operation | Time |
|---|---|---|
| Preexec | Capture command + $EPOCHREALTIME | <0.1ms |
| Detection | Environment variable cascade | ~0.3ms |
| Spawn | Launch suv binary | ~0.5ms |
| DB Open | SQLite connection + WAL | ~0.2ms |
| Insert | Parameterized INSERT | ~0.5ms |
| Cleanup | Process exit | ~0.3ms |
| Total | ~1.9ms | |
Under 2 milliseconds. You won't feel it. Your shell stays as responsive as it was before Suvadu, but now every command is captured with full context, ready to be searched, filtered, and analyzed whenever you need it.
The best developer tools are the ones you forget are running. Suvadu is designed to be invisible until you need it.
Want to try it? Install Suvadu in 30 seconds and start building a better command history.
Madhubalan Appachi
Building developer tools at Appachi Tech. Creator of Suvadu and Kaval.