Import HAR (HTTP Archive) files into SQLite. Query your web traffic with SQL.
HAR files are JSON blobs that capture browser network activity. They're useful for debugging, performance analysis, and understanding how websites work — but querying them is painful:
# The old way: jq gymnastics
cat capture.har | jq '.log.entries[] | select(.response.status >= 400) | {url: .request.url, status: .response.status}'With harlite, import once and query with SQL:
harlite import capture.har
harlite query "SELECT url, status FROM entries WHERE status >= 400"Works great with AI coding agents like Codex and Claude — they already know SQL.
- Fast imports — Rust-native performance
- Smart deduplication — Response bodies stored once using content-addressable hashing (BLAKE3)
- Flexible body storage — Metadata-only by default, opt-in to store bodies
- Optional body decompression — Import gzip/br responses as decoded bytes
- External body extraction — Store body blobs as hashed files on disk (
--extract-bodies) - Full-text search — SQLite FTS5 over response bodies (
harlite search) - Multi-file support — Merge multiple HAR files into one database
- Database merge — Combine multiple harlite databases with deduplication (
harlite merge) - Queryable headers — Headers stored as JSON, queryable with SQLite JSON functions
- Performance analysis — Built-in timing analysis and caching insights (
harlite analyze) - Integrity validation — Check HAR semantics, SQLite integrity, body hashes, and FTS references (
harlite check) - HTML reports — Self-contained HTML report with waterfall + slow/errors (
harlite report) - Interactive REPL — Explore databases with history, completions, and shortcuts (
harlite repl) - Safe sharing — Redact sensitive headers/cookies in HAR files or databases
- PII scanning — Find and redact emails/phones/SSNs/credit cards in HAR files or databases (
harlite pii) - CI regression gates — Enforce performance budgets with
analyzeand change budgets withdiff - Request snippets — Export cURL, Fetch, node-fetch, or PowerShell commands (
harlite request) - Streaming input — Read HAR JSON or compressed HAR data from standard input with
- - Replay — Reissue requests against live servers and compare responses (
harlite replay) - HAR extensions preserved — Store and round-trip HAR 1.3 extension fields as JSON
- CDP capture — Capture from Chrome and write directly to HAR or SQLite
- Watch mode — Monitor a directory and auto-import new HAR files (
harlite watch) - OpenTelemetry export — Export spans to JSON or OTLP (HTTP/gRPC)
- GraphQL indexing — Extract and index operations + top-level fields for filtering
cargo install harlitePublished on crates.io as harlite.
Release archives are available for Linux AMD64/ARM64, macOS AMD64/ARM64, and Windows AMD64 on the GitHub Releases page.
Each archive includes harlite, README.md, CHANGELOG.md, and LICENSE. Releases also include SHA256SUMS and keyless Sigstore-backed GitHub artifact attestations:
grep 'harlite-v0.4.0-linux-amd64.tar.gz' SHA256SUMS | sha256sum --check -
gh attestation verify harlite-v0.4.0-linux-amd64.tar.gz --repo brucehart/harliteBy default, builds use the full feature (all optional commands enabled). To build a smaller binary, disable default features and opt in to what you need:
cargo build --release --no-default-features --features "compression,graphql"Available features:
full: enable all optional commands (default)compression: parse.har.gz/.har.brand support--decompress-bodiesgraphql: parse GraphQL operations and index top-level fieldscdp: Chrome DevTools Protocol capture (harlite cdp)watch: directory watcher imports (harlite watch)repl: interactive SQL REPL (harlite repl)serve: mock HTTP server (harlite serve)replay: replay requests (harlite replay)otel: OpenTelemetry export (harlite otel)completions: shell completions (harlite completions)parquet: Parquet export forharlite export-data
You can capture network traffic directly from a running Chrome instance using the Chrome DevTools Protocol (CDP).
# Start Chrome with remote debugging enabled
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/harlite-profile
# Capture to HAR
harlite cdp --har capture.har
# Capture to SQLite (imports entries directly)
harlite cdp --output capture.db
# Capture both (with bodies)
harlite cdp --har capture.har --output capture.db --bodiesRequired Chrome launch flags:
--remote-debugging-port=9222(or a different port you pass via--port)- A dedicated user data directory (
--user-data-dir=...) to avoid conflicts with existing Chrome sessions
Optional flags:
--host/--portto select the CDP address--targetto match a specific page by id, URL, or title substring--durationto capture for N seconds (otherwise stop with Ctrl+C)--bodies,--text-only,--max-body-sizeto control stored response bodies
Show full CDP options:
harlite cdp --helpgit clone https://github.com/brucehart/harlite
cd harlite
# Requires Rust/Cargo >= 1.85
# Recommended: use rustup to manage toolchains
curl https://sh.rustup.rs -sSf | sh -s -- -y
source "$HOME/.cargo/env"
rustup update stable
# Run without installing
cargo run -- --help
# Or install locally
cargo install --path .
# Or build a release binary
cargo build --release
./target/release/harlite --helpPerformance note: HAR parsing streams JSON from disk, but memory still scales with the number and size of entries imported. Decompressed input is capped at 512 MiB to bound compressed-file expansion. Embedders can choose a lower limit with parse_har_file_with_limit. Use --jobs to parallelize across multiple HAR files (auto by default, capped to limit SQLite contention) and --async-read to read large files via a background reader thread.
Generate completions from the CLI (always in sync with flags/subcommands):
harlite completions bash > ~/.local/share/bash-completion/completions/harlite
harlite completions zsh > ~/.zsh/completions/_harlite
harlite completions fish > ~/.config/fish/completions/harlite.fishFor zsh, ensure your ~/.zsh/completions directory is in fpath (e.g., add fpath=(~/.zsh/completions $fpath) to ~/.zshrc).
PowerShell (save and dot-source in your profile):
harlite completions powershell > $HOME\Documents\PowerShell\Completions\harlite.ps1
". $HOME\Documents\PowerShell\Completions\harlite.ps1" >> $PROFILE# Import a single HAR file (creates capture.db by default)
harlite import browsing-session.har
# Import multiple HAR files into one database
harlite import day1.har day2.har day3.har -o traffic.db
# Query with harlite
harlite query "SELECT method, url, status, time_ms FROM entries LIMIT 10" traffic.db
# Or query with sqlite3 / any SQLite tool
# sqlite3 traffic.db "SELECT method, url, status, time_ms FROM entries LIMIT 10"
# Or use any SQLite tool: DBeaver, datasette, Python, etc.# List GraphQL operations
harlite query "SELECT url, graphql_operation_type, graphql_operation_name FROM entries WHERE graphql_operation_type IS NOT NULL" traffic.db
# Find requests that touch a specific top-level field
harlite query "SELECT e.url, e.graphql_operation_name FROM entries e JOIN graphql_fields gf ON gf.entry_id = e.id WHERE gf.field = 'viewer'" traffic.dbharlite can load default flags and filters from a TOML config file. CLI flags always override config.
Supported filenames:
- Project-local:
.harlitercorharlite.toml(searched from the filesystem root down to the current directory; later files override earlier ones). - User-global:
$XDG_CONFIG_HOME/harlite/harlite.toml(or~/.config/harlite/harlite.toml), plus~/.harliterc. On Windows,%APPDATA%/harlite/harlite.tomlis also checked.
Precedence (lowest to highest):
- User-global config
- Project-local configs from root → current directory
- CLI flags
To see the resolved config (defaults + files), run:
harlite configExample harlite.toml:
[import]
bodies = true
text_only = true
max_body_size = "200KB"
host = ["api.example.com"]
status = [200, 204]
[export]
compact = true
mime = ["json"]
min_response_size = "1KB"
[redact]
header = ["authorization", "x-api-key"]
match_mode = "wildcard"
[pii]
redact = false
no_defaults = false
email_regex = ["(?i)\\b[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}\\b"]harlite supports external plugins for custom import/export logic. Plugins are separate executables that read a JSON request from stdin and write a JSON response to stdout. This keeps a clear security boundary (the plugin runs as its own process).
For full details, see the plugin developer guide: docs/plugin-developer-guide.md.
Plugin kinds:
- filter — decide whether an entry should be included
- transform — modify entries during import/export
- exporter — consume the final HAR and optionally skip the default output
Plugin config example:
[[plugins]]
name = "sample-filter"
kind = "filter"
command = "plugins/sample_filter.py"
phase = "import"
[[plugins]]
name = "sample-exporter"
kind = "exporter"
command = "plugins/sample_exporter.py"
phase = "export"For security, configuration only defines plugins; it never authorizes execution. Every plugin must be explicitly enabled with --plugin on each run. enabled = false can be used as an administrative deny switch.
Enable/disable per run:
harlite import capture.har --plugin sample-filter
harlite export traffic.db --disable-plugin sample-exporter
harlite watch ./captures --plugin sample-filterPlugin API (v1):
Filter request:
{"api_version":"v1","event":"filter_entry","phase":"import","context":{"command":"import","source":"/path/capture.har","database":"/path/output.db","output":null},"entry":{...}}Filter response:
{"allow": true}Transform response:
{"entry": {...}}Exporter request:
{"api_version":"v1","event":"export","phase":"export","context":{"command":"export","source":null,"database":"traffic.db","output":"traffic.har"},"har":{...}}Exporter response (optional):
{"skip_default": true}Sample plugins are in plugins/.
The supported, SemVer-stable API for embedding is exposed via harlite::api
and the convenience harlite::prelude.
use harlite::prelude::*;
use std::path::PathBuf;
fn main() -> Result<()> {
let input = PathBuf::from("capture.har");
let _stats = run_import(&[input], &ImportOptions::default())?;
Ok(())
}# Basic import (creates <filename>.db)
harlite import capture.har
# Specify output database
harlite import capture.har -o mydata.db
# Import multiple files (merges into one database)
harlite import *.har -o all-traffic.db# Watch a folder for new HAR files (imports into traffic.db by default)
harlite watch ./captures -o traffic.db
# Watch recursively and import existing files on startup
harlite watch ./captures --recursive --import-existing -o traffic.db# Merge multiple databases into one (default: <first-input>-merged.db)
harlite merge day1.db day2.db
# Choose output path and dedup strategy
harlite merge day1.db day2.db -o traffic.db --dedup exact
# Dry run to see merge stats without writing
harlite merge day1.db day2.db --dry-runBy default, harlite imports metadata only (URLs, headers, timing, status codes). Response bodies are not stored to keep databases small and imports fast.
# Include text bodies under 100KB (HTML, JSON, JS, CSS, XML)
harlite import capture.har --bodies --text-only
# Include all bodies under 1.5MB
harlite import capture.har --bodies --max-body-size 1.5MB
# Decompress response bodies based on Content-Encoding (gzip, br)
harlite import capture.har --bodies --decompress-bodies
# Keep both decompressed and original (compressed) variants
harlite import capture.har --bodies --decompress-bodies --keep-compressed
# Extract bodies to files (stored by hash); implies --bodies
harlite import capture.har --extract-bodies ./bodies
# Extract only response bodies, with 2-level sharding (aa/bb/<hash>)
harlite import capture.har --extract-bodies ./bodies --extract-bodies-kind response --extract-bodies-shard-depth 2
# Include everything (warning: large databases)
harlite import capture.har --bodies --max-body-size unlimited
# Show deduplication stats after import
harlite import capture.har --bodies --stats
# Output:
# Entries imported: 847
# Unique response bodies: 203
# Space saved by deduplication: 127 MB (74%)Size flags accept decimals and short units (e.g., 1.5MB, 1M, 100k, 500B, unlimited).
Response bodies are automatically deduplicated using BLAKE3 hashing. If the same JavaScript bundle appears in 50 entries, it's stored only once.
Use content hashes to skip entries that are already in the database (helpful for repeated captures or resuming interrupted runs):
# Skip entries already imported (content-hash dedup)
harlite import capture.har -o traffic.db --incremental
# Resume the most recent incomplete import for this source file
harlite import capture.har -o traffic.db --resume--resume reuses the latest non-complete imports record for the same source filename and continues inserting entries that are missing. Progress is tracked in imports.status, imports.entries_total, and imports.entries_skipped.
Filter entries at import time to reduce database size:
# Only keep GETs to a host with 200 responses
harlite import capture.har --host api.example.com --method GET --status 200
# Filter by URL regex (repeatable)
harlite import capture.har --url-regex 'example\\.com/(api|v1)/'
# Import a specific time range (RFC3339 or YYYY-MM-DD)
harlite import capture.har --from 2024-01-15 --to 2024-01-16Speed up multi-file imports by using multiple workers (SQLite writes are still serialized, so keep concurrency modest):
# Auto-select worker count (capped to avoid DB contention)
harlite import day1.har day2.har day3.har -o traffic.db --jobs 0
# Explicitly set concurrency
harlite import day1.har day2.har day3.har -o traffic.db --jobs 4
# Use async file reads for very large HARs
harlite import huge.har -o traffic.db --jobs 2 --async-readIf you imported bodies, harlite maintains a SQLite FTS index over response bodies (text only):
harlite search "timeout NEAR/3 error" traffic.dbTo rebuild the index (or change tokenizers):
harlite fts-rebuild traffic.db --tokenizer porterIf your database stores extracted bodies on disk, you must opt-in to reading them:
harlite fts-rebuild traffic.db --allow-external-paths --external-path-root ./bodies# Print the SQLite schema
harlite schema
# Print schema as it exists in a database
harlite schema traffic.db# Show summary statistics for a database
harlite info traffic.db
# Output:
# Database: traffic.db
# Imports: 3 files
# Entries: 1,247
# Date range: 2024-01-15 to 2024-01-17
# Unique hosts: 23
# Stored blobs: 156 (12.4 MB)harlite stats is a faster, script-friendly alternative to harlite info.
harlite stats traffic.db
# imports=3
# entries=1247
# date_min=2024-01-15
# date_max=2024-01-17
# unique_hosts=23
# blobs=156
# blob_bytes=13002342
# JSON output
harlite stats traffic.db --jsonUse harlite analyze to summarize performance timings, slow requests, connection reuse, and caching opportunities.
harlite analyze traffic.db
# JSON output for scripts
harlite analyze traffic.db --json
# Filters + thresholds
harlite analyze traffic.db --host api.example.com --from 2024-01-15 --to 2024-01-16 \
--slow-total-ms 800 --slow-ttfb-ms 300 --top 20
# Fail a CI job when a budget is exceeded
harlite analyze traffic.db --json --max-p95-total-ms 800 \
--max-p95-ttfb-ms 300 --max-errors 0Check HAR semantics or database integrity before analysis, sharing, or release automation:
harlite check capture.har
harlite check traffic.db --strict
harlite check traffic.db --json
# External bodies are never read unless explicitly trusted
harlite check traffic.db --allow-external-paths --external-path-root ./extracted-bodiesList import metadata (id, source, date range, entry count):
harlite imports traffic.dbRemove a specific import and its entries/pages/blobs:
harlite prune traffic.db --import-id 2
# Also delete external body files, restricted to a trusted root
harlite prune traffic.db --import-id 2 --allow-external-paths \
--external-path-root ./extracted-bodiesExport a harlite SQLite database back to HAR format (optionally with bodies if they were stored during import):
# Export all entries (pretty-printed by default)
harlite export traffic.db -o traffic.har
# Export to stdout
harlite export traffic.db -o -
# Include stored request/response bodies (if present in the DB)
harlite export traffic.db --bodies -o traffic-with-bodies.har
# Prefer raw/compressed response bodies when available (requires import with --decompress-bodies --keep-compressed)
harlite export traffic.db --bodies-raw -o traffic-with-raw-bodies.har
# If bodies were extracted to disk, opt in to reading them
harlite export traffic.db --bodies --allow-external-paths --external-path-root ./bodies -o traffic-with-bodies.har
# Compact JSON
harlite export traffic.db --compact -o traffic.min.har
# Filter examples
harlite export traffic.db --host api.example.com --status 200 --method GET -o api-get-200.har
harlite export traffic.db --url-regex 'example\\.com/(api|v1)/' -o filtered.har
harlite export traffic.db --from 2024-01-15 --to 2024-01-16 -o day1.har
harlite export traffic.db --ext js,css -o assets.har
harlite export traffic.db --source session1.har --source-contains chrome -o sources.har
harlite export traffic.db --mime json --min-response-size 1KB --max-response-size 200k -o api-responses.harCommon filters:
--url,--url-contains,--url-regex--host,--method,--status--mime(substring match),--ext(file extension)--from/--to(RFC3339 timestamp orYYYY-MM-DD)--min-request-size/--max-request-size,--min-response-size/--max-response-size--source/--source-contains(filters byimports.source_file)
Notes / gaps:
- HAR
timingsare reconstructed from the stored total duration (time_ms), so the breakdown is best-effort. - Some HAR fields are not stored in the DB (e.g.
headersSize, responsehttpVersion), so they may be omitted or approximated on export. --bodies-rawuses the raw/compressed response body (if stored), setscontent.encodingwhen base64-encoded, and fillscontent.compressionwhen the uncompressed size is known.- Extension fields (including underscore-prefixed fields) on log/page/entry/request/response/content/timings/postData are preserved as JSON for round-trip export. Example extensions seen in Chromium HARs include
_resourceType,_priority,_transferSize,_initiator,_fromDiskCache, and_fromServiceWorker. - Schema upgrades automatically add extension columns (
log_extensions,page_extensions,entry_extensions, etc.) when opening older databases.
Export entries as CSV, JSON Lines, or Parquet (feature-gated). Filters match harlite export.
# JSON Lines export (default)
harlite export-data traffic.db -o entries.jsonl
# CSV export
harlite export-data traffic.db --format csv -o entries.csv
# Parquet export (requires building with the parquet feature)
cargo build --features parquet
harlite export-data traffic.db --format parquet -o entries.parquet
# Filter examples
harlite export-data traffic.db --host api.example.com --status 200 --method GET
harlite export-data traffic.db --url-regex 'example\\.com/(api|v1)/'
harlite export-data traffic.db --from 2024-01-15 --to 2024-01-16 --format csvGenerate an OpenAPI 3.0 schema from captured traffic. By default, the schema is conservative and does not sample bodies. Use --sample-bodies to infer JSON schemas.
harlite openapi traffic.db -o openapi.json
# Sample up to 5 request/response bodies per operation (JSON only)
harlite openapi traffic.db --sample-bodies 5 --sample-body-max-size 100KB -o openapi.json
# Filter to a specific host and time window
harlite openapi traffic.db --host api.example.com --from 2024-01-15 --to 2024-01-16 -o openapi.jsonExport request waterfall timing data as either a Chrome/Perfetto trace (machine-readable) or a terminal-friendly ASCII diagram:
# Machine-readable trace (JSON)
harlite waterfall traffic.db --format trace --group-by page -o trace.json
# Terminal-friendly ASCII diagram
harlite waterfall traffic.db --format text --group-by navigation
# Filter examples
harlite waterfall traffic.db --host api.example.com --from 2024-01-15 --to 2024-01-16
harlite waterfall traffic.db --page "Homepage"Export timing data as OpenTelemetry spans, either as JSON (for inspection) or directly to an OTLP collector.
# JSON export to stdout
harlite otel traffic.db --format json
# JSON export to file
harlite otel traffic.db --format json -o traces.json
# OTLP over HTTP (protobuf)
harlite otel traffic.db --format otlp-http --endpoint http://localhost:4318
# OTLP over gRPC
harlite otel traffic.db --format otlp-grpc --endpoint localhost:4317
# Include only specific hosts and keep the first 5k spans
harlite otel traffic.db --host api.example.com --max-spans 5000
# Disable phase spans (only root request spans)
harlite otel traffic.db --no-phasesSampling / volume notes:
- Exports can be large; use
--sample-rateto reduce volume deterministically and--max-spansto cap output. - Each request becomes a root span; with phases enabled, extra child spans are emitted for blocked/dns/connect/ssl/send/wait/receive.
Compare two HAR files or two harlite databases to find added/removed requests, timing deltas, and header/body size changes:
# Compare two HAR files
harlite diff before.har after.har
# Compare two databases
harlite diff before.db after.db --format json
# Filter to specific hosts/paths/statuses
harlite diff before.har after.har --host api.example.com --method GET --status 200 --url-regex 'example\\.com/api/'
# Ignore cache-busting parameters and fail on new errors or slowdowns
harlite diff before.har after.har --ignore-query-param cacheBust \
--fail-on new-errors --max-total-regression-ms 100 --max-new-errors 0Matching is done by (method, url) with a stable ordinal match per pair: if the same method+URL appears multiple times, the first occurrence in the left file is matched with the first in the right, the second with the second, and so on (ordered by HAR entry order or started_at for databases).
Replay requests from a HAR file or database against live servers, then compare status/headers/body size:
# Replay a HAR against live servers (GET/HEAD/OPTIONS/TRACE only by default)
harlite replay capture.har --method GET --format table
# Replay and allow unsafe methods (POST/PUT/DELETE/PATCH)
harlite replay capture.har --allow-unsafe
# Replay from a database with filters and concurrency
harlite replay traffic.db --host api.example.com --status 200 --concurrency 8 --rate-limit 20
# Override host or headers during replay
harlite replay capture.har --override-host 'example\\.com=staging.example.com:8443'
harlite replay capture.har --override-header 'Authorization=Bearer token'
harlite replay capture.har --override-header 'example\\.com:Authorization=Bearer token'Safety: unsafe methods are skipped unless --allow-unsafe is set. Automatic redirects are disabled so captured credentials and custom sensitive headers cannot be forwarded to another origin.
Redact common sensitive headers/cookies (by default: authorization, cookie, set-cookie, x-api-key, etc.) before sharing:
# Modify a database in-place
harlite redact traffic.db
# Write to a new database (recommended)
harlite redact traffic.db --output traffic.redacted.db
# Dry run (no writes)
harlite redact traffic.db --dry-run
# Customize patterns (wildcard match by default)
harlite redact traffic.db --no-defaults --match exact --header authorization --cookie sessionid
# Wildcard / regex name matching
harlite redact traffic.db --match wildcard --header '*token*'
harlite redact traffic.db --match regex --header '^(authorization|x-api-key)$'
# Redact URL query parameters by name
harlite redact traffic.db --query-param token --query-param session --match wildcard
# Redact matching patterns in stored bodies (UTF-8 only)
harlite redact traffic.db --body-regex '(?i)\"password\"\\s*:\\s*\"[^\"]+\"'
# Read externally stored bodies only from an explicit trusted root
harlite redact traffic.db --body-regex 'secret' --allow-external-paths \
--external-path-root ./extracted-bodies
# HAR files are written to a new sibling file by default
harlite redact capture.har
harlite redact capture.har --output capture.safe.har
# Stream a redacted HAR to stdout; status details go to stderr
cat capture.har | harlite redact - --output - > capture.safe.harRedaction removes superseded blobs and compacts the database, including its WAL, so old values are not left in ordinary SQLite free pages. External body paths are ignored unless explicitly enabled and contained by the selected root.
Find emails, phone numbers, SSNs, and credit card numbers in URLs and stored bodies:
# Scan and report findings in a database or HAR file
harlite pii traffic.db
harlite pii capture.har
# JSON output for scripting
harlite pii traffic.db --format json
# Customize patterns (disable defaults, add your own)
harlite pii traffic.db --no-defaults --email-regex '(?i)\\b.+@example\\.com\\b'
# Auto-redact findings (write a new DB or HAR)
harlite pii traffic.db --redact --output traffic.redacted.db
harlite pii capture.har --redact --output capture.redacted.har
# Scan trusted externally stored bodies
harlite pii traffic.db --allow-external-paths --external-path-root ./extracted-bodiesDefaults are conservative but may still produce false positives; review results before redacting. Use --no-defaults to opt out and supply your own regexes.
Turn captured requests into reproducible commands. Credential-bearing and transport-managed headers are excluded unless --include-sensitive is explicitly supplied.
harlite request capture.har --format curl --limit 5
harlite request traffic.db --format fetch --host api.example.com
harlite request capture.har --format node-fetch --index 2
harlite request capture.har --format powershell --output requests.ps1Use - where a compatible command expects a HAR path. Commands that create a file require an explicit output path because no filename can be derived from stdin.
cat capture.har | harlite import - --output traffic.db
cat capture.har | harlite check - --strict
cat capture.har | harlite request - --format curl
cat capture.har | harlite report - --output report.htmlRun ad-hoc SQL against a harlite SQLite database and format the results:
# Default output: table with headers
harlite query "SELECT method, url, status FROM entries LIMIT 5" traffic.db
# CSV / JSON output (includes headers / keys)
harlite query "SELECT host, COUNT(*) AS n FROM entries GROUP BY host" traffic.db --format csv
harlite query "SELECT host, COUNT(*) AS n FROM entries GROUP BY host" traffic.db --format json
# Apply limit/offset without editing your SQL (wraps the query)
harlite query "SELECT * FROM entries ORDER BY started_at" traffic.db --limit 100 --offset 200
# If you omit the database path, harlite will use the only *.db in the current directory (if exactly one exists)
harlite query "SELECT COUNT(*) AS entries FROM entries" --format jsonCSV fields beginning with spreadsheet formula characters are emitted as text to prevent formula execution when opened in spreadsheet applications.
Start an interactive SQL shell with readline history, tab completion, and shortcut commands:
# Start a REPL (defaults to table output)
harlite repl traffic.db
# Start in JSON mode
harlite repl traffic.db --format jsonInside the REPL:
.help Show commands
.mode csv Switch output mode (table/csv/json)
.slow 20 Top 20 slowest requests
.status Status code counts
.tables List tables
.exit Quit
The main table containing one row per HTTP request/response pair.
| Column | Type | Description |
|---|---|---|
id |
INTEGER | Primary key |
import_id |
INTEGER | References imports.id |
page_id |
TEXT | References pages.id (if available) |
started_at |
TEXT | ISO 8601 timestamp |
time_ms |
REAL | Total request duration in milliseconds |
blocked_ms |
REAL | Time spent blocked (ms) |
dns_ms |
REAL | DNS lookup time (ms) |
connect_ms |
REAL | TCP connect time (ms) |
send_ms |
REAL | Request send time (ms) |
wait_ms |
REAL | Time to first byte (TTFB) (ms) |
receive_ms |
REAL | Response receive time (ms) |
ssl_ms |
REAL | TLS handshake time (ms) |
method |
TEXT | HTTP method (GET, POST, etc.) |
url |
TEXT | Full request URL |
host |
TEXT | Hostname extracted from URL |
path |
TEXT | Path extracted from URL |
query_string |
TEXT | Query string (without leading ?) |
http_version |
TEXT | HTTP version (HTTP/1.1, h2, etc.) |
request_headers |
TEXT | Request headers as JSON object |
request_cookies |
TEXT | Request cookies as JSON array |
request_body_hash |
TEXT | BLAKE3 hash referencing blobs.hash |
request_body_size |
INTEGER | Request body size in bytes |
status |
INTEGER | HTTP response status code |
status_text |
TEXT | HTTP response status text |
response_headers |
TEXT | Response headers as JSON object |
response_cookies |
TEXT | Response cookies as JSON array |
response_body_hash |
TEXT | BLAKE3 hash referencing blobs.hash |
response_body_size |
INTEGER | Response body size in bytes |
response_body_hash_raw |
TEXT | Raw/compressed body hash (when stored) |
response_body_size_raw |
INTEGER | Raw/compressed body size (when stored) |
response_mime_type |
TEXT | Response MIME type |
is_redirect |
INTEGER | 1 if 3xx redirect, 0 otherwise |
server_ip |
TEXT | Server IP address (if available) |
connection_id |
TEXT | Connection ID (if available) |
entry_hash |
TEXT | Stable content hash (used for incremental imports) |
entry_extensions |
TEXT | Entry extension fields (JSON) |
request_extensions |
TEXT | Request extension fields (JSON) |
response_extensions |
TEXT | Response extension fields (JSON) |
content_extensions |
TEXT | Content extension fields (JSON) |
timings_extensions |
TEXT | Timings extension fields (JSON) |
post_data_extensions |
TEXT | PostData extension fields (JSON) |
Content-addressable storage for request/response bodies. Bodies are deduplicated by hash.
| Column | Type | Description |
|---|---|---|
hash |
TEXT | BLAKE3 hash (primary key) |
content |
BLOB | Raw body content |
size |
INTEGER | Content size in bytes |
mime_type |
TEXT | MIME type (if known) |
external_path |
TEXT | External blob path (if extracted) |
Page/document information from the HAR (if present).
| Column | Type | Description |
|---|---|---|
id |
TEXT | Page ID from HAR |
import_id |
INTEGER | References imports.id |
started_at |
TEXT | Page load start time |
title |
TEXT | Page title |
on_content_load_ms |
REAL | DOMContentLoaded timing |
on_load_ms |
REAL | Window load timing |
page_extensions |
TEXT | Page extension fields (JSON) |
page_timings_extensions |
TEXT | Page timings extension fields (JSON) |
Tracks import history for auditing and multi-file management.
Use harlite imports to list these records and harlite prune --import-id <id> to remove a specific import.
| Column | Type | Description |
|---|---|---|
id |
INTEGER | Primary key |
source_file |
TEXT | Original HAR path (canonicalized when possible) |
imported_at |
TEXT | Import timestamp |
entry_count |
INTEGER | Number of entries imported |
log_extensions |
TEXT | Log extension fields (JSON) |
status |
TEXT | Import status (in_progress or complete) |
entries_total |
INTEGER | Total entries detected in the source |
entries_skipped |
INTEGER | Entries skipped by incremental dedup |
The following indexes are created for fast queries:
idx_entries_url— URL lookups and LIKE queriesidx_entries_host— Filter by domainidx_entries_status— Filter by status codeidx_entries_method— Filter by HTTP methodidx_entries_mime— Filter by content typeidx_entries_started— Time range queriesidx_entries_import— Filter by import sourceidx_entries_entry_hash— Incremental import lookups
SELECT method, url, status, time_ms
FROM entries
WHERE time_ms > 1000
ORDER BY time_ms DESC;SELECT method, url, status, response_body_size
FROM entries
WHERE url LIKE '%/api/%'
ORDER BY started_at;SELECT host, COUNT(*) as count, AVG(time_ms) as avg_time_ms
FROM entries
GROUP BY host
ORDER BY count DESC;SELECT method, url, status, status_text
FROM entries
WHERE status >= 400
ORDER BY status;SELECT url, response_mime_type, response_body_size
FROM entries
WHERE response_body_size IS NOT NULL
ORDER BY response_body_size DESC
LIMIT 20;SELECT e.url, e.status, b.content
FROM entries e
JOIN blobs b ON e.response_body_hash = b.hash
WHERE e.url LIKE '%/api/users%';Identify responses that appear multiple times (useful for finding redundant API calls or cached resources):
SELECT
b.hash,
b.size,
b.mime_type,
COUNT(*) as times_seen,
GROUP_CONCAT(DISTINCT e.host) as hosts
FROM blobs b
JOIN entries e ON e.response_body_hash = b.hash
GROUP BY b.hash
HAVING COUNT(*) > 1
ORDER BY b.size * COUNT(*) DESC;SELECT
SUM(e.response_body_size) as total_if_duplicated,
(SELECT SUM(size) FROM blobs) as actual_stored,
SUM(e.response_body_size) - (SELECT SUM(size) FROM blobs) as bytes_saved
FROM entries e
WHERE e.response_body_hash IS NOT NULL;SELECT url, json_extract(response_headers, '$.content-type') as content_type
FROM entries
WHERE response_mime_type LIKE '%json%';SELECT * FROM entries
WHERE started_at BETWEEN '2024-01-15T10:00:00' AND '2024-01-15T11:00:00';SELECT DISTINCT method, host, path
FROM entries
WHERE host = 'api.example.com'
ORDER BY path;SELECT
url,
json_extract(response_headers, '$.cache-control') as cache_control,
json_extract(response_headers, '$.content-encoding') as encoding
FROM entries
WHERE json_extract(response_headers, '$.cache-control') IS NOT NULL;SELECT
i.source_file,
COUNT(*) as entries,
MIN(e.started_at) as first_request,
MAX(e.started_at) as last_request
FROM entries e
JOIN imports i ON e.import_id = i.id
GROUP BY i.id;Agent-specific repository instructions live in AGENTS.md.
harlite is designed to work seamlessly with AI coding assistants:
# Import your browsing session
harlite import session.har -o api.db
# Ask Codex/Claude to analyze
# "Query api.db to find all POST requests to endpoints containing 'user'
# and show me the request bodies"The AI can write SQL directly — no need to learn a custom query language.
- Start with metadata-only imports — faster iteration
- Use
harlite infoto give the AI context about what's in the database - Import with
--bodies --text-onlywhen you need to analyze API responses - The schema is stable — AI can learn it once and reuse queries
Datasette provides an instant web UI for exploring SQLite databases:
pip install datasette
harlite import capture.har -o traffic.db
datasette traffic.db
# Opens browser to http://localhost:8001# CSV export
sqlite3 -header -csv traffic.db "SELECT url, status FROM entries" > results.csv
# JSON export
sqlite3 -json traffic.db "SELECT url, status FROM entries" > results.json# Import from multiple HAR files
harlite import monday.har tuesday.har wednesday.har -o week.db
# Query across all sessions
sqlite3 week.db "SELECT source_file, COUNT(*) FROM entries GROUP BY source_file"# Skip bodies entirely for fastest import
harlite import huge-capture.har
# Or limit body size
harlite import huge-capture.har --bodies --max-body-size 10KB --text-only
# For very large captures, try async reads with modest parallelism
harlite import huge-capture.har --async-read --jobs 2Requirements:
- Rust 1.85+
git clone https://github.com/brucehart/harlite
cd harlite
cargo build --release
# Run tests
cargo test
# Install locally
cargo install --path .MIT
Contributions welcome! Please open an issue to discuss major changes before submitting a PR.
*Created by Bruce Hart