Technical 12 min read

How Suvadu Records Shell History with Under 2ms Overhead

A technical deep-dive into how Suvadu captures every shell command in under 2 milliseconds using Rust, SQLite WAL mode, and native Zsh hooks.

Madhubalan Appachi ·

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:

  1. Preexec hook (before command runs): Capture the command string and record start time
  2. Command executes: Suvadu is not involved here at all
  3. 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.

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:

  1. Spawning the suv binary (~0.5ms on a warm filesystem cache)
  2. Opening the SQLite database connection (~0.2ms)
  3. Executing a single parameterized INSERT (~0.5ms)
  4. 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.

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.

The synchronous=NORMAL pragma is equally important. The default FULL mode calls fsync() after every transaction (5-20ms). NORMAL only syncs at checkpoint boundaries.

The Insert Query

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)

The Database Schema

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,
    ended_at     INTEGER NOT NULL,
    duration_ms  INTEGER NOT NULL,
    context      TEXT,
    tag_id       INTEGER REFERENCES tags(id),
    executor_type TEXT,
    executor     TEXT
);

CREATE INDEX idx_entries_started_at ON entries(started_at);
CREATE INDEX idx_entries_command    ON entries(command);
CREATE INDEX idx_entries_session_id ON entries(session_id);
CREATE INDEX idx_entries_tag_id    ON entries(tag_id);

The 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.

A B-tree index on command cannot accelerate LIKE '%query%' searches because the leading wildcard prevents prefix matching. However, the index is still valuable for prefix-based queries like arrow key navigation (LIKE 'git comm%'), and SQLite's query planner may use it for covering index scans on simpler queries.

Synchronous by Design

The suv add call is synchronous. It blocks the shell until the write completes. If async, pressing up arrow immediately after a command would sometimes miss the most recent entry. The 1.5ms of synchronous blocking is worth the guarantee.

The Search Side

Search needs to feel instant. Suvadu's TUI uses ratatui and renders on every keystroke:

  1. User types in the search box
  2. A parameterized SQL query fires with LIKE '%input%' plus any active filters
  3. Results are paginated with LIMIT/OFFSET
  4. 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, still under 10ms.

Arrow Key Frecency

SELECT command FROM entries
WHERE command LIKE 'git comm%'
  AND cwd = '/Users/me/project'
ORDER BY started_at DESC
LIMIT 1 OFFSET 3

The prefix match (LIKE 'query%' without a leading wildcard) can use the B-tree index on command.

Putting It All Together

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.

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.

#rust#sqlite#performance#shell-hooks#architecture#zsh