Skip to content
Technical

How Suvadu Records Shell History with <2ms Overhead

By Madhubalan Appachi12 min read

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

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

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.

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:

  1. User types in the search box
  2. A parameterized SQL query fires with LIKE '%input%' plus any active filters (date range, tag, exit code, executor, directory)
  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, 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:

StageOperationTime
PreexecCapture command + $EPOCHREALTIME<0.1ms
DetectionEnvironment variable cascade~0.3ms
SpawnLaunch suv binary~0.5ms
DB OpenSQLite connection + WAL~0.2ms
InsertParameterized INSERT~0.5ms
CleanupProcess 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.

Share this article

PostShare
👣

Madhubalan Appachi

Building developer tools at Appachi Tech. Creator of Suvadu and Kaval.

Related Posts