rano is a network observer for AI CLI processes that tracks observed sockets owned by Claude Code, Codex CLI, Gemini CLI, and their descendants—live in your terminal and durably in SQLite when logging is enabled.
This README describes the development source. The latest published release checked on 2026-10-14 is v0.2.0; the source version 0.2.1 is unreleased. The observation, durability, configuration and reporting repairs described here are not promises about the older release binary. Independent verification and whole-project qualification remain open; see the dated reality check for the original failures and proof requirements.
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bashThe Problem: AI CLI tools spawn subprocesses and open sockets you can’t easily attribute back to a provider or process in real time.
The Solution: rano polls Linux /proc (or macOS libproc), maps sockets to PIDs, and prints observation events with provider tags while logging the observed history to SQLite. Connections that open and close between polls can be missed.
| Feature | What It Does |
|---|---|
| Provider-aware | Tags observed endpoint episodes as anthropic, openai, google, or unknown. |
| Descendant-aware | Follows child processes automatically. |
| Live stats | In-terminal summaries with top IPs/domains and provider totals. |
| SQLite logging | Queryable history with built-in aggregate views. |
| Flexible output | Pretty or JSON output + optional file logging. |
# Observe default AI CLIs (claude/codex/gemini)
./rano
# Narrow to Codex + log JSON
rano --pattern codex --json --log-file /tmp/rano.log
# Run with per-run log files
rano --log-dir /tmp/rano-logs --log-format json
# Track a specific PID (no descendants)
rano --pid 1234 --no-descendants
# Emit a single poll and exit
rano --once
# View SQLite aggregates
sqlite3 observer.sqlite "select * from provider_counts;"- Explicit attribution: preserve observed socket owners, monitored roots and provider-match origins; a shared owner does not prove who initiated a socket.
- Low overhead: polling
/proc(Linux) or libproc (macOS) avoids ptrace or intrusive hooks; workload measurements and their limits are documented below. - Human-first output: readable live output with clear provider tags and colors.
- Durable observed history: enabled SQLite logging reports persistence and loss explicitly; polling can miss sockets between observations.
| Feature | rano | lsof + grep | ss/netstat | tcpdump/wireshark |
|---|---|---|---|---|
| PID → socket mapping | ✅ Built-in | ✅ but manual | ❌ not PID-aware | |
| Provider attribution | ✅ tags | ❌ | ❌ | ❌ |
| Live stats | ✅ | ❌ | ||
| Durable history | ✅ SQLite | ❌ | ❌ | ✅ (pcap) |
| Low privileges | ✅ (PTR mode) | ✅ | ✅ | ❌ (often root) |
When to use rano: monitor AI CLI network behavior during dev or audits.
When it might not fit: you need full packet capture, payload inspection, or decrypted TLS analysis.
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bashWith options:
# Specific version
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bash -s -- --version v0.1.0
# System-wide (requires sudo)
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | sudo bash -s -- --system
# Auto-update PATH in shell rc files
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bash -s -- --easy-mode
# Build the exact release tag from source
bash install.sh --from-source --version v0.2.0 --verify
# Explicit development source ref; resolved commit and binary checksum are reported
bash install.sh --from-source --ref main --verifyAn unresolved latest release or failed archive download exits unsuccessfully; the installer never invents a prerelease version or silently substitutes a different source HEAD. --from-source --version selects that exact tag, while --ref explicitly selects a source branch, full ref (such as refs/tags/v0.2.0) or commit and cannot combine with --version. A bare non-commit name denotes a branch. Source builds use Cargo's emitted executable path, including redirected CARGO_TARGET_DIR and target triples. They require Git, Cargo and Python 3 for artifact metadata parsing.
Archive installation requires a valid SHA256 using sha256sum or stock macOS shasum, a supported regular-file archive layout and a binary whose reported version matches the selected release. --artifact-url, --checksum and --checksum-url support explicit archives and checksum sidecars. Candidates are verified before installation, and verification failures return nonzero; prior destination bytes and temporary/source files are retained for recovery. --quiet suppresses ordinary output, and startup files are changed only with explicit --easy-mode. The updater completes its installer download before execution and passes custom OWNER/REPO and literal arguments to the installer process.
Native monitoring has two backends with the same events and output:
- Linux reads
/proc: process records,/proc/<pid>/fdsocket links and the/proc/net/{tcp,tcp6,udp,udp6}tables. - macOS (since 0.2.1) reads the whole process table with
sysctl(KERN_PROC_ALL)and arguments withKERN_PROCARGS2. For each target it lists socket descriptors withproc_pidinfo(PROC_PIDLISTFDS)and reads each socket's TCP/UDP endpoints and state withproc_pidfdinfo(PROC_PIDFDSOCKETINFO).
The monitor checks its backend before starting capture or creating SQLite/file output, and refuses with an actionable error (backend_unavailable in JSON diagnostics) if the check fails:
- Linux: readable self process identity, descriptors, network namespace and the selected TCP/UDP tables.
- macOS: its own process record, plus a loopback UDP probe socket (connected, never sent on) that must decode with the expected addresses, ports and protocol. A future kernel with a different socket record layout therefore fails closed instead of reporting zero sockets.
Target-specific visibility failures during monitoring remain explicit observation errors and may coexist with valid observations of other targets. On macOS, an unprivileged observer cannot list the descriptors of other users' processes (root daemons, for example). Each such target is reported as an observation error, like an unreadable /proc/<pid>/fd on Linux.
| Command | Linux | macOS |
|---|---|---|
| Monitor (default command) | Requires readable Linux procfs and target visibility | libproc backend; targets must belong to the observing user (or run rano as root) |
| report/export/diff | SQLite history, independent of procfs | SQLite history |
| status | Current activity requires a fresh observable Linux process; history remains available | Same contract, with the macOS process record as the observer identity |
| config commands | Independent of procfs | Same |
Backend differences in recorded fields:
- The ownership JSON's
inodefield holds the kernel's opaque socket handle on macOS; there are no socket inodes there. start_timeis the process start time in clock ticks since boot on Linux, and in microseconds since the Unix epoch on macOS.- TCP states use the Linux codes on both platforms (
01established,0Alisten, ...). - An IPv6 socket carrying IPv4 traffic is reported as an IPv4-mapped IPv6 address, as Linux
tcp6/udp6do.
WSL2 runs the Linux backend; targets must be visible inside that Linux environment and its network namespace.
Windows is not supported. rano's observer backends use Linux /proc or macOS
libproc, and Unix libc functions (getnameinfo, localtime_r) that have no
Windows equivalents. On Windows, use
WSL 2 and install the Linux
binary inside the WSL environment.
git clone https://github.com/Dicklesworthstone/rano
cd rano
cargo build --release
cp target/release/rano ~/.local/bin/cargo install --git https://github.com/Dicklesworthstone/rano --locked- Run with defaults (auto-monitors claude/codex/gemini):
rano
- Target a process name:
rano --pattern codex
- Log to SQLite (default
observer.sqlite):rano --sqlite /tmp/rano.sqlite
- See aggregates:
sqlite3 /tmp/rano.sqlite "select * from provider_counts;"
Monitor observed sockets and print events/stats. TCP peer ownership does not distinguish an outgoing connection from an accepted connection.
rano --pattern claude --pattern codex
rano --exclude-pattern browser
rano --pid 1234 --no-descendants
rano --stats-interval-ms 2000 --stats-width 60
rano --log-dir /tmp/rano-logs --log-format json
rano --onceCommon flags
--pattern <str> # Repeatable process/cmdline substring
--exclude-pattern <str> # Repeatable exclusion filter
--pid <pid> # Repeatable PID targeting
--no-descendants # Only exact PIDs
--interval-ms <ms> # Polling interval
--json # Emit JSON lines to stdout
--summary-only # Suppress live events
--domain-mode <auto|ptr|pcap>
--pcap # Force pcap mode (falls back)
--pcap-interface <name> # Capture interface for pcap mode (default: auto)
--pcap-cache-max <n> # DNS/SNI cache cap, >= 100 (default: 10000)
--no-dns # Disable PTR lookups (pcap evidence can still apply)
--include-udp # Include UDP sockets (default: enabled)
--no-udp # Exclude UDP sockets
--include-listening # Include TCP listening sockets
--log-file <path> # Append output to a file
--log-dir <path> # Per-run log files
--log-format <auto|pretty|json>
--sqlite <path> # SQLite file for event logging
--no-sqlite # Disable SQLite
--db-batch-size <n> # SQLite batch size (events per transaction)
--db-flush-ms <ms> # SQLite flush interval in ms
--db-queue-max <n> # SQLite queue capacity (events)
--stats-interval-ms <ms> # Live stats cadence (default: 5000; 0 disables)
--stats-width <n> # ASCII bar width
--stats-top <n> # Top-N lists size
--stats-view <name> # Stats view provider|domain|port|process (repeatable)
--stats-cycle-ms <ms> # Rotate stats views at this interval (0 disables)
--show-ancestry # Capture/display process ancestry chains (off by default)
--session-name <name> # Override auto-generated session name
--retry-threshold <n> # Connections within window before retry warning (default 3)
--retry-window-ms <ms> # Retry detection window in ms (default 60000)
--once # Single poll and exit
--color <auto|always|never>
--no-color # Disable ANSI color
--theme <vivid|mono|colorblind>
--no-banner
--config <path> # Load key=value config
--config-toml <path> # Load provider TOML config
--no-configrano can resolve domains in two modes:
- PTR (default): reverse DNS lookups on two background workers. No elevated privileges required; target process descriptors must still be readable.
- pcap: uses libpcap to capture DNS responses + TLS SNI. Requires a build with
--features pcapand root/CAP_NET_RAW for live capture. If capture fails, rano logs a warning and falls back to PTR automatically.
PTR lookup never waits on the polling thread. A new endpoint initially has
unknown hostname evidence; a completed lookup can emit domain_update for a
held endpoint and correct its original SQLite row, reports and exports. A
one-poll --once run can finish before PTR completes. Queue saturation or an
unresolved lookup leaves the hostname unknown. The final ptr health object
reports requests, completions, unresolved/deferred work, pending age and cache
occupancy. Scratch state is limited to 64 queued requests, two workers and 1,024
cached positive/negative results with a 600-second TTL; accurate lifetime
statistics still grow with distinct observed identities and names. Pending
lookups cannot delay shutdown, though a blocked system call may occupy either
worker until it returns. --alert-unknown-domain includes pending PTR results.
Every resolved domain carries a provenance source so you can weigh attribution confidence:
domain_source |
Meaning | Observation scope |
|---|---|---|
sni |
Visible hostname in a captured TLS ClientHello | Full local/remote socket tuple; observed inode and capture time checked |
dns |
One unexpired hostname candidate from captured DNS answers | Host-global answer IP; does not prove this process queried that name |
ptr |
Reverse DNS on the remote IP | IP-level fallback; CDN/shared hosts may be misleading |
| (absent) | Unresolved or attribution unavailable | — |
The source appears as "domain_source" in --json events, in SQLite
(events.domain_source), in exports, and as a [sni]-style suffix on report
Top Domains rows.
Socket ownership metadata includes domain_evidence: observation scope, capture_kind
(live, offline, or none), the original capture timestamp, ambiguity flags,
the number of valid DNS candidates, and a sorted sample of up to eight names.
Multiple DNS candidates remain ambiguous; PTR can still provide a separately labeled
fallback. DNS records expire at their observed TTL, capped at five minutes; SNI
cache entries expire after ten minutes. Expired/future captures, older reordered
SNI messages, known inode changes and retired tuples cannot supply an old association.
A hostname already observed on the same held inode remains historical evidence
after cache expiry. Sockets that disappear entirely between polls remain outside
the observer's coverage.
Report domain/provider groups containing multiple provenance kinds are labeled
mixed; a single SNI row does not promote other DNS/PTR/unresolved observations
in that group to SNI evidence.
Late associations emit domain_update records to live output and file logs, linked
by the observation ID inside decoded ownership_json. SQLite corrects that
original connect row rather than inserting another connection. Exports and reports see the corrected
provenance with unchanged connect/close/provider counts; metadata-write failure is
reported separately and makes the run unsuccessful.
Pcap capture supports Ethernet frames with up to two VLAN tags; the capture filter
admits untagged, single-tagged and double-tagged DNS/TLS traffic. Other datalink
types produce an actionable startup warning and fallback. Live idle timeouts keep
the capture worker running. Final summaries expose pcap packet/mapping/delivery/
drain/loss counters, kernel/interface drop snapshots, state and errors. Mapping
loss or a runtime capture failure warns, falls back to PTR (or unresolved names
with --no-dns) and ends with a nonzero status while preserving valid socket
observations. Offline EOF is successful completion; truncated-file read errors
are failures. Queue delivery/drain counters describe parsed messages, not a
guarantee that every message contains unexpired hostname evidence.
Examples:
# Prefer pcap attribution (falls back if unavailable)
rano --domain-mode pcap
rano --pcap --pattern claude
# Force PTR mode (no packet capture)
rano --domain-mode ptrOffline replay requires a pcap-enabled build and a regular capture file; it does
not require root. It preserves original capture timestamps and streams all
messages through a bounded queue, including with --once. Old fixture records
may be expired and never become fresh evidence simply because they are replayed:
RANO_PCAP_FILE=tests/fixtures/pcap/dns-fixture.pcap \
rano --pcap --once --json --no-bannerThe pcap E2E script generates current packet fixtures for real held sockets and also tests real live capture inside a private Linux network namespace. TLS extraction requires a complete visible ClientHello in one captured transport payload. Segmented handshakes, encrypted ClientHello inner names and encrypted DNS are not reconstructed; an outer SNI name is not proof of an ECH inner name.
Update the installed binary.
rano update
rano update --version v0.1.0
rano update --system
rano update --owner your-org --repo rano --branch mainGenerate reports from SQLite event history.
# Show latest session report
rano report --latest
# Show specific session by run_id
rano report --run-id "12345-1705512345000"
# Filter by time range
rano report --since 24h # Last 24 hours
rano report --since 7d # Last 7 days
rano report --since 2026-01-17 # Since date
rano report --since 2026-01-17T10:00:00Z # Since timestamp
# Combine filters
rano report --since 2026-01-17 --until 2026-01-18
# Output options
rano report --latest --json # JSON output
rano report --latest --top 5 # Top 5 domains/IPs
rano report --sqlite /path/to/rano.sqlite # Custom database
# Color control
rano report --latest --color alwaysReport sections
- Session: run ID, start/end times, duration, host, patterns
- Summary: total events, connects, closes, filtered connect-close balance, observed event peak, and average/maximum valid close duration with sample count
- Providers: event/connect/close counts and distinct observed domains and peer IPs per provider
- Top Domains: most observed domain/provider groups, including unresolved
unknowngroups - Top IPs: most observed peer IPs with a stored representative domain when available
- Hourly Activity: UTC hour bins with event counts per provider
All report sections use one database read snapshot. JSON schema meta.version = "2.0" labels filtered metrics separately from full-session metadata. Durations include only nonnegative integer duration_ms values on close rows; absent samples produce null, while a measured zero remains zero. Domain/IP counts exclude unknown domains and absent peer addresses. Count ties sort by name, and an IP with several known domains uses the alphabetically first as its display representative.
summary.peak_active reconstructs the stored connect/close ledger in UTC timestamp and event-ID order, carrying earlier history into a selected window. Aggregate reports take the maximum per session; they never add separate observers together. This event-order peak can differ from the monitor's reconciled poll-boundary peak and cannot recover connections missed by polling. Missing opening history, invalid timestamps, absent run identity or event ordering, and windows with no endpoint events produce a typed peak_unavailable_reason. Unfiltered reports count malformed-time rows in unbucketed_events; time-filtered queries refuse timestamps they cannot order. The filtered connect-close balance is historical arithmetic and can be negative when a window contains only closes.
Time range formats
- Relative:
1h,24h,7d,30m,1w - Date:
2026-01-17(interpreted as midnight UTC) - RFC3339:
2026-01-17T10:00:00Z
Dates are checked against the Gregorian calendar. Timestamps accept a known numeric offset or Z, lower-case t/z, and up to nine fractional digits; query comparisons normalize both stored timestamps and boundaries to fixed nanosecond UTC precision. Leap-second inputs and the unknown offset -00:00 are rejected explicitly. Relative arithmetic is checked, and --since must precede --until; the start is inclusive and the end exclusive.
--latest selects the newest session by metadata start time, including a valid empty session, with run ID as a deterministic tie-breaker. An events-only legacy database falls back to each run's latest normalized event. Explicit run IDs and unambiguous session names are accepted by report/export/diff; a missing reference or duplicate name fails with an actionable error. Diff thresholds must be finite and within 0..100, and lists/count ties have stable ordering.
Compare two monitoring sessions and highlight behavioral changes.
# Compare two run IDs
rano diff --old "abc123" --new "def456"
# Lower significance threshold and output JSON
rano diff --old "abc123" --new "def456" --threshold 25 --json
# Compare named sessions
rano diff --old "morning-claude-audit-2026-01-20" --new "afternoon-claude-audit-2026-01-20"Diff sections
- New domains: domains present in new session but not old
- Removed domains: domains present in old session but not new
- Changed domains: domains whose counts change above threshold
- New processes: process names that appear only in new session
- Removed processes: process names that appear only in old session
- Provider changes: significant per-provider count shifts
One-line status output for shell prompt integration (PS1, starship, etc.).
# Default one-line format
rano status
# Custom format template
rano status --format '{active}/{total}'
# Custom SQLite database
rano status --sqlite /path/to/rano.sqliteTemplate variables
| Variable | Description |
|---|---|
{active} |
Endpoint count in a fresh valid poll from the live observer |
{total} |
Historical endpoint observations this session |
{anthropic} |
Historical Anthropic endpoint observations |
{openai} |
Historical OpenAI endpoint observations |
{google} |
Historical Google endpoint observations |
{session_name} |
Current session name (if available) |
{state} |
fresh, ended, stale, degraded, unverified, or unavailable |
Default format: {active} active | anthropic:{anthropic} openai:{openai}
Activity comes from the monitor's published poll count, with its host and PID/kernel-start-time identity checked. Its observation timestamp must be no older than the greater of five seconds or three configured poll intervals; a blocked or stopped monitor cannot refresh it by publishing old observations later. Finalized sessions always return zero current activity without inventing close events. Legacy history lacks this proof and returns zero activity with unverified; its totals and name remain available. A newer empty session still takes precedence. Counts and metadata are read from one SQLite snapshot while the monitor keeps writing. Database/query errors are returned explicitly.
Shell integration examples
# Bash PS1
PS1='$(rano status 2>/dev/null) \$ '
# Zsh RPROMPT
RPROMPT='$(rano status 2>/dev/null)'
# Starship custom command
[custom.rano]
command = "rano status 2>/dev/null"
when = "command -v rano"Performance: Status reads a consistent database snapshot and checks observer identity. Query time depends on the database and host; the original <50 ms design target is not a current latency qualification.
rano can alert when observed endpoint episodes match domain patterns or reach concurrent-count thresholds. Pretty alerts appear on stderr; JSON alerts share stdout with observation records.
# Watch for connections to suspicious domains
rano --alert-domain "*.evil.com" --alert-domain "*.malware.org"
# Alert when concurrently observed endpoints reach 100
rano --alert-max-connections 100
# Alert when any provider reaches 50 concurrently observed endpoints
rano --alert-max-per-provider 50
# Alert when an endpoint closes after more than 30 seconds
rano --alert-duration-ms 30000
# Alert on endpoints without resolved hostname evidence
rano --alert-unknown-domain
# Combine multiple alerts for security monitoring
rano --alert-domain "*.suspicious.com" \
--alert-max-connections 200 \
--alert-unknown-domain \
--alert-bell| Flag | Description |
|---|---|
--alert-domain <pattern> |
Glob pattern for domains to watch (repeatable) |
--alert-max-connections <N> |
Alert when total active connections reach N (fires at >= N) |
--alert-max-per-provider <N> |
Alert when any provider reaches N concurrently observed connections (fires at >= N) |
--alert-duration-ms <N> |
Alert when an endpoint closes with duration greater than N ms |
--alert-unknown-domain |
Alert on connections to unresolved IPs |
--alert-bell |
Ring terminal bell on alert |
--alert-cooldown-ms <N> |
Suppress duplicate alerts within N ms (default: 10000) |
--no-alerts |
Disable all alerting |
Global and provider thresholds are evaluated once after each poll has reconciled new and closed sockets. A replacement in the same poll does not create a transient breach. Ongoing breaches and re-crossings can alert again after the configured cooldown; per_provider retains lifetime totals, while active_per_provider in the final JSON summary reports current counts. Peak activity is sampled at the same poll boundary.
Alerts print to stderr with timestamps and details:
[ALERT] 2026-01-20T10:00:00Z | CRITICAL | domain_match | evil.malicious.com matched *.malicious.com
[ALERT] 2026-01-20T10:00:01Z | WARNING | max_connections | 101/100 active connections
[ALERT] 2026-01-20T10:00:02Z | WARNING | long_duration | 45000ms > 30000ms
In JSON mode, alerts are JSON objects on stdout, interleaved with connection events so machine consumers read a single stream (pretty-mode alerts go to stderr):
{"type":"alert","ts":"2026-01-20T10:00:00Z","kind":"domain_match","severity":"critical","pattern":"*.malicious.com","domain":"evil.malicious.com"}Rows are flagged with alert=1 only when an alert actually emitted — repeats
suppressed by --alert-cooldown-ms leave the flag unset, so SQL counts match what
you saw on screen. Threshold alerts (--alert-max-connections,
--alert-max-per-provider) have no natural connection row; each emitted breach
writes a dedicated row with event='alert' and NULL pid:
-- Find all emitted alerts (connect/close flags plus threshold breaches)
SELECT * FROM events WHERE alert = 1;
-- Count alerts by domain
SELECT domain, COUNT(*) as alerts FROM events WHERE alert = 1 GROUP BY domain;
-- Threshold breaches land as their own event kind
SELECT ts, provider FROM events WHERE event = 'alert';Security audit: Watch for unexpected outbound connections during a code review session.
rano --pattern claude \
--alert-domain "*.unknown-cdn.com" \
--alert-unknown-domain \
--alert-max-connections 50Rate limit detection: Identify when AI CLI tools are making too many connections.
rano --alert-max-per-provider 100 \
--alert-duration-ms 60000Compliance monitoring: Log all connections that match suspicious patterns.
rano --alert-domain "*.cn" \
--alert-domain "*.ru" \
--sqlite /var/log/rano-audit.sqliteAlerts aren't firing
- Verify the pattern matches:
rano --alert-domain "*.example.com" --onceshould show the flag is accepted - Check cooldown: default is 10 seconds. Use
--alert-cooldown-ms 1000for more frequent alerts - Ensure
--no-alertsis not set - For domain alerts, ensure hostname evidence is available (
--no-dnsdisables PTR only; pcap may still supply DNS/SNI)
Too many alerts (spam)
- Increase cooldown:
--alert-cooldown-ms 60000(60 seconds) - Narrow patterns: use more specific domain patterns
- Raise thresholds: increase
--alert-max-connectionsvalue
rano's second design pillar is low overhead. docs/perf.md separates historical results from the repaired counted benchmark and a progressing 600-second default/no-DNS soak. scripts/bench.sh measures complete invocations with and without DNS/SQLite; its paced durable workload is not maximum writer capacity. Broader PID/endpoint diversity, PTR, ancestry and pcap resource qualification remains open. A performance win requires candidate and incumbent in the same invocation.
As checked on 2026-10-14, the general CI, distribution, release automation and installer-notification workflows are disabled manually. The active perf workflow is nonblocking and cannot establish a current whole-project gate. Their source files or historical green jobs are not evidence of a currently qualified release.
rano watches for connection-churn storms (rapid reconnects to the same endpoint from the same PID) and warns once per endpoint per window.
# Warn when 5+ closes to one endpoint occur within 30 seconds
rano --pattern claude --retry-threshold 5 --retry-window-ms 30000| Flag | Default | Description |
|---|---|---|
--retry-threshold <n> |
3 | Connections within the window that trigger a warning |
--retry-window-ms <ms> |
60000 | Detection window; also acts as the per-source re-warn cooldown |
- Warnings print as yellow
⚠ Retry pattern:lines on stderr in pretty mode. - In
--jsonmode they are emitted as{"type":"retry_warning","count":N,"endpoint":"ip:port","window_seconds":S}on stdout, alongside events and alerts. - The triggering close event persists
retry_countin SQLite (events.retry_count) for post-hoc queries.
AI CLI invocations routinely embed credentials in argv — ANTHROPIC_API_KEY=sk-... claude, wrapper scripts passing tokens, CI jobs echoing keys. By default rano records full cmdlines into SQLite, log files, and exports, which turns a security-audit tool into a credential sink.
--redact-cmdline masks cmdlines in durable stores only (SQLite rows, log files, exports). Live terminal display and provider attribution always see the original argv, so classification and on-screen output are unchanged.
Masking preserves /proc argument boundaries, including empty arguments and values with spaces or newlines. A separate --password value is masked in full. Invalid UTF-8 is displayed with replacement characters. Session argument snapshots use the same masking; stored patterns and session names also mask recognized credential tokens, and all removes their values.
| Mode | Behavior |
|---|---|
off (default) |
Store cmdlines verbatim |
secrets |
Mask KEY=value assignments whose key mentions key/token/secret/password/credential, --api-key <value> / --token=value style flags, known token prefixes (sk-, ghp_, xox, AKIA), JWT-shaped blobs, and high-entropy arguments |
all |
Replace the whole cmdline with <redacted:N args> |
# Audit preset enables this by default; equivalently:
rano --preset audit
rano --pattern claude --redact-cmdline # = secrets
rano --pattern claude --redact-cmdline all # maximum paranoiaThreat model: redaction protects readers of rano's own artifacts (a shared observer.sqlite, an exported CSV attached to a ticket, log archives) from inheriting every secret that transited a monitored shell. It cannot protect against the monitored processes leaking their own secrets elsewhere.
secrets uses named credential flags, assignments, known prefixes and token-shape heuristics. It cannot recognize arbitrary credentials embedded in scripts, free text, filenames or unrecognized flags; use all when the entire command line must be hidden. Live stdout remains unmasked, so capturing terminal output creates an unprotected artifact.
The built-in audit preset enables redact_cmdline=secrets.
rano includes built-in configuration presets for common use cases. Presets bundle multiple settings into a single flag, making it easy to switch between monitoring styles without remembering individual options.
| Preset | Description | Key Settings |
|---|---|---|
audit |
Security review / minimal noise | summary_only=true, stats_interval_ms=0, include_udp=true, no_dns=false, log_format=json, redact_cmdline=secrets |
quiet |
Reduce terminal output | stats_interval_ms=0, no_banner=true, summary_only=true |
live |
Real-time monitoring focus | stats_interval_ms=500, stats_top=10, stats_view=provider,domain, stats_cycle_ms=5000 |
verbose |
Maximum detail | include_udp=true, include_listening=true, stats_interval_ms=1000, stats_top=10 |
# Security audit preset - sampled observations with minimal output noise
rano --preset audit --pattern claude
# Quiet mode - background monitoring
rano --preset quiet --log-file /tmp/rano.log
# Live monitoring - see stats frequently
rano --preset live
# Verbose mode - show all details including UDP and listening sockets
rano --preset verbose
# List available presets
rano --list-presets
# Combine presets - later preset wins for conflicting settings
rano --preset audit --preset verbose
# Override preset values with CLI flags
rano --preset verbose --stats-interval-ms 5000| Flag | Description |
|---|---|
--preset <name> |
Load a preset configuration (repeatable) |
--list-presets |
Show all available presets and exit |
Create custom presets in ~/.config/rano/presets/:
mkdir -p ~/.config/rano/presets
# Create a custom preset
cat > ~/.config/rano/presets/myaudit.conf << 'EOF'
# Description: Custom audit for my workflow
include_udp=true
include_listening=true
stats_interval_ms=2000
stats_top=20
summary_only=false
EOFThe first line comment # Description: ... becomes the preset description shown in --list-presets.
Preset file format:
- One
key=valueper line - Lines starting with
#are comments - Uses the same keys as config files (see Configuration section)
- Boolean values:
true/false,yes/no,on/off, or1/0
When multiple presets are specified, they're applied in order. Later presets override earlier ones:
# audit sets stats_interval_ms=0
# verbose sets stats_interval_ms=1000
# Result: stats_interval_ms=1000 (verbose wins)
rano --preset audit --preset verboseCLI flags always override preset values:
# verbose sets stats_interval_ms=1000
# CLI sets stats_interval_ms=5000
# Result: stats_interval_ms=5000 (CLI wins)
rano --preset verbose --stats-interval-ms 5000Preset not found
error: Unknown preset 'mypreset'. Available: audit, live, quiet, verbose
- Check the preset name spelling
- Ensure the preset file exists:
ls ~/.config/rano/presets/ - Verify file extension is
.conf - Run
rano --list-presetsto see available presets
Preset not loading values
- Check file format: must be
key=value; whitespace around the key and value is trimmed - Verify key names match config options (see Configuration section)
- For boolean values, use
true/false,yes/no,on/off, or1/0
rano can record the observed process ancestry chain with --show-ancestry. It helps identify a socket owner's monitored tree; shared ownership does not prove which process initiated the connection.
For each tracked process, rano walks visible parent identities and reverses the result into root-to-owner order. The chain can stop before PID 1 when an ancestor is unavailable. The ancestry is stored as a comma-separated path:
init:1,systemd:500,sshd:1200,bash:1500,tmux:1600,bash:1700,claude:2000
Each entry is comm:pid where comm is the process name and pid is the process ID.
In live output, long ancestry chains are truncated to keep output readable:
init(1) → systemd(500) → bash(1700) → claude(2000)
Chains longer than 5 levels show ellipsis: ... → bash(1700) → claude(2000)
The full ancestry path is stored in the ancestry_path column:
Ancestry capture is gated behind --show-ancestry (off by default for
performance). When it is disabled — or cannot be resolved — ancestry_path
is stored as SQL NULL, so filter with plain null checks:
-- Ancestry present (works regardless of --show-ancestry)
SELECT * FROM events WHERE ancestry_path IS NOT NULL;-- Find all connections from processes spawned by tmux
SELECT * FROM events WHERE ancestry_path LIKE '%tmux%';
-- Group connections by their spawn path
SELECT ancestry_path, COUNT(*) as connections
FROM events
GROUP BY ancestry_path
ORDER BY connections DESC;
-- Find connections from a specific process tree
SELECT * FROM events WHERE ancestry_path LIKE '%claude:1234%';The ancestry path is included in CSV and JSONL exports:
# Export with ancestry
rano export --format csv > connections.csv
# Filter and analyze ancestry in JSON
rano export --format jsonl | jq 'select((.ancestry_path // "") | contains("tmux"))'Attribution: Determine which terminal session or tmux pane initiated a connection.
Security audit: Verify that AI CLI processes are being launched from expected parent processes.
Debugging: Trace why a particular subprocess made an unexpected connection.
Process tree analysis: Identify patterns in how AI CLIs spawn their network-making children.
Each poll uses a process snapshot with kernel start times. Ancestry is cached for at most 30 seconds and reused only while every process identity and comm in the chain is unchanged; PID reuse and reparenting invalidate it. Ancestry capture is active when --show-ancestry is passed; without it the stored ancestry_path is null. Display truncates chains deeper than five levels to ... → grandparent → leaf, while JSON and SQLite retain the full chain. Reserved path characters (%, comma, colon and line breaks) in comm names use percent escapes so process names remain unambiguous.
Exclusions prune the matching process and its descendants, including explicitly selected PIDs and nested pattern matches. The nearest selected ancestor defines each monitored root. For a socket shared by multiple monitored processes, the lowest PID is the primary owner. ownership_json in JSON events, SQLite and exports contains the socket inode, owner/root PID and kernel start time, and all observed monitored holders; decode this field as JSON to inspect shared ownership. PID reuse, inode replacement or a change of monitored root starts a new observation episode rather than reusing stale metadata. These records describe sampled ownership; they do not prove which holder initiated a shared socket.
Provider matching uses original comm/argv before redaction. A unique direct match wins even when the monitored ancestor belongs to another provider. An unmarked or ambiguous process inherits the nearest uniquely matched ancestor within the monitored tree; an unclassifiable root stays unknown. Nested roots and simultaneous providers are resolved per process. ownership_json.provider_origin identifies the supplying PID/start time and whether the match was direct or inherited from an ancestor; the owner and monitored root remain separate identities. Changes of provider or origin start a new observation episode.
TCP endpoint episodes emit connect/close, but /proc does not establish whether the process initiated or accepted them: ownership_json.direction is unknown. --include-listening admits TCP listeners with a listen event. UDP is included by default; connected UDP has a known peer, while a bound/sendto socket emits bind with no observed remote endpoint (remote="none", remote_present=false). The kernel's zero remote address/port remain in SQLite/export, alongside this explicit absence flag. A sendto destination is not recoverable from /proc; no hostname is inferred for local-only observations. --no-udp excludes both UDP kinds.
ownership_json.socket_kind is endpoint, listener or bound_udp, and kernel_state preserves the sampled hexadecimal state. Summary connects, closes, active, provider counts and connection alerts concern endpoint episodes; listens/binds count local-only observations and socket_active/socket_closes include all kinds. Namespace or socket-table/read-permission failures produce visible diagnostics, increment observation_errors, and make the final exit unsuccessful. This backend observes the observer's network namespace and reports when a selected process is elsewhere. A successful empty poll proves only that no admissible sockets were observed in that poll.
rano can export SQLite event history to CSV or JSONL for use with external tools like Excel, Pandas, jq, or data pipelines.
# Export all events to CSV (Excel-compatible)
rano export --format csv > connections.csv
# Export to JSONL for shell pipelines
rano export --format jsonl | jq '.domain' | sort | uniq -c
# Filter by time range
rano export --format csv --since 24h > daily.csv
rano export --format csv --since 2026-01-20 --until 2026-01-21 > jan20.csv
# Filter by provider
rano export --format csv --provider anthropic > anthropic.csv
# Filter by domain pattern
rano export --format csv --domain "*.openai.com" > openai.csv
# Custom SQLite file
rano export --format csv --sqlite /path/to/observer.sqlite > export.csv| Flag | Description |
|---|---|
--format csv|jsonl |
Output format (required) |
--sqlite <path> |
SQLite database (default: observer.sqlite) |
--since <timestamp> |
Events after timestamp (RFC3339, date, or relative like 24h) |
--until <timestamp> |
Events before timestamp |
--run-id <id> |
Filter by session run ID |
--provider <name> |
Filter by provider (anthropic, openai, google, unknown) |
--domain <pattern> |
Filter by domain glob pattern (repeatable) |
--fields <list> |
Override default field list (comma-separated) |
--no-header |
Omit CSV header row |
--output <path> / -o <path> |
Write to a file instead of stdout; replaces an existing destination |
Both CSV and JSONL include these fields (in order for CSV):
| Field | Type | Description |
|---|---|---|
ts |
string | ISO 8601 timestamp |
run_id |
string | Session identifier |
event |
string | connect, close, listen, bind, or synthesized threshold alert |
provider |
string | anthropic, openai, google, unknown |
pid |
integer | Process ID; absent on synthesized threshold alerts |
comm |
string | Process command name |
cmdline |
string | Stored command line, subject to the monitor's redaction mode |
proto |
string | tcp or udp |
local_ip |
string | Local IP address |
local_port |
integer | Local port |
remote_ip |
string | Remote IP address |
remote_port |
integer | Remote port |
domain |
string | Resolved domain name (if available) |
domain_source |
string | sni, dns, or ptr when attributed |
ancestry_path |
string | Process ancestry chain (comm:pid,comm:pid,...); NULL unless --show-ancestry is enabled |
duration_ms |
integer | Connection duration in ms (close events only) |
alert |
integer | 1 when an alert emitted for this event (cooldown-suppressed alerts stay 0) |
retry_count |
integer | Connections-in-window at retry-warning trigger time (close events only) |
ownership_json |
string | Encoded JSON with observation/owner/root identity, socket kind, peer presence, direction and hostname evidence; decode separately |
CSV
- RFC 4180 compliant with CRLF line endings
- Fields with commas, quotes, or newlines are properly escaped
- UTF-8 encoded, compatible with Excel
JSONL
- One JSON object per line
- LF line endings
- Null fields are omitted
- UTF-8 encoded, ideal for
jqpipelines
Daily summary to spreadsheet
rano export --format csv --since 24h > "$(date +%Y-%m-%d)-connections.csv"Find unique domains by provider
rano export --format jsonl --provider anthropic | jq -r '.domain // empty' | sort -uCount connections per domain
rano export --format jsonl | jq -r '.domain // "unknown"' | sort | uniq -c | sort -rn | head -20Import to Pandas
import pandas as pd
df = pd.read_csv("connections.csv")
df.groupby("provider")["event"].count()rano provides built-in configuration validation to catch errors and typos before runtime. The config subcommand helps you verify, inspect, and troubleshoot your configuration.
# Validate all config files
rano config check
# Validate every source a run would load, including overrides
rano config check --config-toml /path/to/custom.toml
RANO_CONFIG_TOML=/path/to/custom.toml rano config check
rano config check --config /path/to/rano.conf
# Show resolved configuration
rano config show
# Show configuration as JSON
rano config show --json
# Inspect the same presets and overrides a monitor would use
rano config show --json --preset live --interval-ms 100
rano config check --preset live --interval-ms 100
# List config file search paths
rano config paths| Subcommand | Description |
|---|---|
config check |
Resolve and validate the same selected files, ordered presets and monitor options; report errors/warnings |
config show |
Display effective monitor settings, provider matcher and source origins |
config show --json |
Typed effective settings, providers, origins, sources and warnings; recognized credentials are masked |
config paths |
Show where rano looks for config files |
Both check and show accept monitor options. show --json selects the inspection
format; use show --json -- --json to also inspect JSON monitoring. Inspection
does not open output sinks or start capture. Runtime file/database permissions and
capture availability are checked when monitoring starts.
Settings resolve as defaults, the selected KV file, presets in their given order,
then CLI options. A later source replaces a list setting; repeated list keys or
flags within that source append. An invalid selected source fails even if a later
option would replace its value. Unknown file keys remain visible warnings.
--no-config suppresses KV/TOML discovery and explicit/env file overrides;
explicitly requested presets still load. Missing explicit or environment-selected
sources are errors.
Inspection omits raw source contents and masks recognized credential flags,
assignments and token shapes even when monitoring uses redact_cmdline=off.
Original values still drive matching. Arbitrary credentials embedded in unrecognized
free text or filenames remain outside this heuristic policy.
Errors (prevent rano from starting):
1 error(s) found:
✗ /home/user/.config/rano/config.conf:3: 'interval_ms' must be a non-negative integer, got 'abc'
Warnings (rano will run but may not behave as expected):
1 warning(s) found:
⚠ /home/user/.config/rano/config.conf:5: unknown key 'intervall_ms' (possible typo?)
| Check | Example Error |
|---|---|
| Unknown keys | unknown key 'intervall_ms' (possible typo?) |
| Invalid numbers | 'interval_ms' must be a non-negative integer, got 'abc' |
| Invalid booleans | 'json' must be a boolean (true/false/yes/no/1/0), got 'maybe' |
| Invalid enum values | 'domain_mode' must be one of [auto, ptr, pcap], got 'invalid' |
| Zero where >= 1 required | 'db_batch_size' must be >= 1, got 0 |
| Invalid stats_view | 'stats_view' contains invalid value 'invalid' |
| TOML syntax errors | Invalid table or array syntax |
| Invalid provider mode | providers.mode must be one of [merge, replace], got 'bad' |
| Missing parent directory | log_file parent directory '/nonexistent' does not exist |
-
Create or edit config:
vim ~/.config/rano/config.conf -
Validate before running:
rano config check
-
If errors, fix and re-validate:
1 error(s) found: ✗ config.conf:3: 'json' must be a boolean... -
Once valid, run rano:
rano --pattern claude
| Issue | Fix |
|---|---|
unknown key 'intervall_ms' |
Fix typo: interval_ms |
must be a boolean |
Use true, false, 1, 0, yes, no, on, or off |
must be one of [auto, ptr, pcap] |
Use a valid value from the list |
TOML parse error |
Check TOML syntax (missing quotes, brackets) |
must be >= 1 |
Use a positive integer (not 0) |
Config validation is tested via the E2E harness:
scripts/e2e/run.sh config-validation scripts/e2e/config-validation.shLogs are written to logs/e2e/config-validation-*.log.
The KV path is --config <path>, then a set RANO_CONFIG, then
$XDG_CONFIG_HOME/rano/config.conf (or ~/.config/rano/config.conf when XDG is unset).
User presets use the same configuration directory under rano/presets/ and override
builtins with the same name.
# Process matching
pattern=codex,claude
exclude_pattern=browser
# Polling
interval_ms=1000
# Output
json=false
summary_only=false
color=auto
log_dir=/tmp/rano-logs
log_format=pretty
# Domains
no_dns=false
include_udp=true
include_listening=false
# SQLite
sqlite=/tmp/rano.sqlite
no_sqlite=false
db_batch_size=200
db_flush_ms=1000
db_queue_max=10000
# Stats
stats_interval_ms=2000
stats_width=50
stats_top=5
# UI
no_banner=false
theme=vividSQLite writes use an async batching system for better performance under high event rates.
Default settings:
| Flag | Default | Description |
|---|---|---|
db_batch_size |
200 | Events per transaction |
db_flush_ms |
1000 | Maximum time before flush |
db_queue_max |
10000 | Event queue capacity |
Tuning guidance:
- High event rate (>500 events/sec): Increase
db_batch_sizeto 500-1000, increasedb_queue_maxto 50000 - Low latency queries: Decrease
db_flush_msto 100-500 - Memory constrained: Decrease
db_queue_maxto 1000-5000 - SSD storage: Defaults work well
- HDD storage: Increase
db_batch_sizeto 500+
If the queue fills, events are dropped with a warning. The final JSON summary reports sqlite.observed, persisted, queue_lost, and write_lost; after shutdown, observed events equal persisted events plus both loss counts. Synthesized alert rows are included. sqlite_dropped includes queue and failed-batch losses. Any loss, database startup/finalization failure, or requested file creation/write failure produces a nonzero exit status. Database degradation remains visible in the final summary even when monitoring continues.
Customize provider pattern matching without recompiling. rano searches for config at:
~/.rano.toml~/.config/rano/rano.toml./rano.toml--config-toml <path>(CLI)RANO_CONFIG_TOML(env)
These TOML sources apply in the listed order; an environment-selected file has the highest precedence. Missing default files are ignored. Missing explicit/env sources, unreadable selected files, invalid TOML or invalid values fail resolution. --no-config suppresses all TOML sources. The XDG location replaces ~/.config/rano/ when XDG_CONFIG_HOME is set.
Example rano.toml:
[providers]
# "merge" extends defaults; "replace" clears and uses only specified patterns
mode = "merge"
# Patterns are case-insensitive substrings matched against comm+cmdline
anthropic = ["acme-claude", "internal-claude"]
openai = ["company-codex"]
google = ["corp-gemini"]Merge mode (default): Adds your patterns to the built-in defaults (claude, codex, gemini, etc.).
Replace mode: Clears all defaults and uses only your specified patterns. Useful for strict environments.
Default patterns:
| Provider | Default patterns |
|---|---|
| anthropic | claude, anthropic |
| openai | codex, openai |
gemini, google |
Inspection: Run with --once while the selected process owns a held socket to see provider attribution. A successful empty poll is not proof of attribution:
rano --pattern myprocess --once --json┌──────────────────────────────┐
│ Target Processes │
│ claude / codex / gemini PIDs │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ /proc or libproc Poller │
│ sockets ↔ inodes ↔ PIDs │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Provider Attribution │
│ comm/cmdline → provider tag │
└──────────────┬───────────────┘
│
┌─────────┴─────────┐
▼ ▼
┌──────────────┐ ┌────────────────┐
│ Live Output │ │ SQLite Logger │
│ pretty/json │ │ events + views │
└──────────────┘ └────────────────┘
You requested pcap mode. Run as root or grant CAP_NET_RAW, or switch to PTR mode.
rano will log the warning and fall back to PTR automatically.
rano --domain-mode ptrYour patterns didn’t match any running processes. List processes or add --pid.
ps aux | rg -i codex
rano --pid <pid>Read-side commands (status, report, export) use a five-second SQLite busy timeout. The monitor initializes with that timeout, then limits event writes to three attempts with a 250ms busy timeout and 25ms/50ms backoff. Permanent errors are not retried; exhausted transactions count the whole rolled-back batch as write loss, preserve the error, and make the final status nonzero. If another process holds a long write lock, use a separate database file or coordinate access:
rano --sqlite /tmp/rano-$(date +%s).sqlitePTR lookups are disabled or reverse DNS doesn’t resolve for that IP.
Edit your config:
no_dns=falseStats are suppressed when JSON output is enabled or when --stats-interval-ms 0.
rano --stats-interval-ms 2000rano provides four live stats views that display accumulated observation counts as colorized bar charts. Views can cycle automatically or be displayed individually. Provider/domain/IP totals describe observations since startup; the separate active/peak figures describe reconciled poll counts, and neither recovers traffic missed between polls.
| View | Description | Use Case |
|---|---|---|
provider |
Connections grouped by AI provider (anthropic, openai, google, unknown) | See which AI services are being used |
domain |
Top domains by accumulated endpoint observation count | Identify frequently observed endpoints |
port |
Top destination ports | Monitor protocol distribution |
process |
Top process names (comm) | See which executables are connecting |
# Single view - provider stats only
rano --stats-view provider
# Multiple views - cycle between them
rano --stats-view provider --stats-view domain --stats-cycle-ms 5000
# All views cycling every 3 seconds
rano --stats-view provider --stats-view domain --stats-view port --stats-view process --stats-cycle-ms 3000
# Customize display
rano --stats-view domain --stats-top 10 --stats-width 60 --stats-interval-ms 1000| Flag | Default | Description |
|---|---|---|
--stats-view <view> |
provider | View to display (repeatable for cycling) |
--stats-interval-ms <ms> |
5000 | How often to refresh stats (0 disables) |
--stats-cycle-ms <ms> |
0 | Cycle between views (0 = no cycling) |
--stats-top <n> |
5 | Number of items to show in domain/port/process views |
--stats-width <n> |
40 | Width of bar charts in characters |
Provider View (--stats-view provider):
Live Stats [provider]
anthropic █████████████████████████████████████░░░ 42 (domains=3, ips=2)
openai ████████████████░░░░░░░░░░░░░░░░░░░░░░░░ 18 (domains=2, ips=2)
google ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 6 (domains=1, ips=1)
unknown ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 (domains=0, ips=0)
active=12 peak=45 avg_dur=250ms
Domain View (--stats-view domain --stats-top 5):
Live Stats [domain]
42 ████████████████████████████████████████ api.anthropic.com
18 █████████████████░░░░░░░░░░░░░░░░░░░░░░░ api.openai.com
6 █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ generativelanguage.googleapis.com
3 ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ www.google.com
1 █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ github.com
Port View (--stats-view port):
Live Stats [port]
65 ████████████████████████████████████████ 443
4 ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 80
1 █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8080
Process View (--stats-view process):
Live Stats [process]
35 ████████████████████████████████████████ claude
20 ██████████████████████░░░░░░░░░░░░░░░░░░ node
10 ███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ curl
5 █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ codex
The stats views are tested via the E2E harness:
# Run the stats views E2E test
scripts/e2e/run.sh stats-views tests/e2e/stats-views.sh
# View test output
cat logs/e2e/stats-views-*.logThe test verifies that all four view types render correctly and cycle in sequence.
Stats settings can be set in ~/.config/rano/config.conf:
stats_interval_ms=1000
stats_width=50
stats_top=10
stats_view=provider,domain
stats_cycle_ms=5000Or via presets:
# The 'live' preset enables frequent stats
rano --preset live # stats_interval_ms=500, stats_top=10; provider/domain cycle every 5000 msThe pcap test uses the shared harness for current offline packets and live capture in a private Linux network namespace. It requires a pcap-enabled binary; the live portion requires capture/network-namespace privileges. It writes logs to logs/e2e/:
scripts/e2e/run.sh pcap-attribution scripts/e2e/pcap-attribution.shLogs:
logs/e2e/pcap-attribution-<timestamp>.loglogs/e2e/outputs/for full command output
- Perfect hostname attribution: pcap relies on observed DNS/SNI; some flows still resolve via PTR or remain
unknown. - Packet-level inspection: it’s not a packet sniffer; use tcpdump/wireshark for payloads.
- Windows not supported: monitoring relies on Linux
/procor macOS libproc and Unix libc APIs; use WSL 2 on Windows. - macOS needs ownership for visibility: an unprivileged observer sees the sockets of its own user's processes only; other users' targets are reported as observation errors.
Short for rust_agent_network_observer.
No for PTR mode. Pcap mode uses libpcap and typically needs root/CAP_NET_RAW, but it
falls back to PTR automatically if capture is unavailable. Offline pcap replay can
be done without privileges via RANO_PCAP_FILE.
rano --pid 1234 --no-descendantsrano --json --log-file /tmp/rano.jsonl--log-file writes a single file; --log-dir creates per-run files. SQLite logs default to observer.sqlite.
About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via
ghand independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
See LICENSE, titled “MIT License (with OpenAI/Anthropic Rider),” for the full terms. Cargo metadata currently declares MIT; the LICENSE file also contains the additional rider and restrictions.
