Skip to content
DicklesworthstonePublic

About

Network observer that tracks outbound connections from AI CLI processes (Claude Code, Codex, Gemini), attributing sockets to providers in real time with SQLite logging

Topics

Resources

Stars

31 stars

Watchers

1 watching

Forks

Repository files navigation

rano

rano - network observer for AI CLI processes

Release

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.

Quick Install

curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bash

TL;DR

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

Why Use rano?

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.

Quick Example

# 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;"

Design Philosophy

  1. Explicit attribution: preserve observed socket owners, monitored roots and provider-match origins; a shared owner does not prove who initiated a socket.
  2. Low overhead: polling /proc (Linux) or libproc (macOS) avoids ptrace or intrusive hooks; workload measurements and their limits are documented below.
  3. Human-first output: readable live output with clear provider tags and colors.
  4. Durable observed history: enabled SQLite logging reports persistence and loss explicitly; polling can miss sockets between observations.

How rano Compares

Feature rano lsof + grep ss/netstat tcpdump/wireshark
PID → socket mapping ✅ Built-in ✅ but manual ⚠️ limited ❌ not PID-aware
Provider attribution ✅ tags ❌ ❌ ❌
Live stats ✅ ❌ ⚠️ basic ⚠️ manual
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.


Installation

Quick Install (Recommended)

curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/rano/main/install.sh | bash

With 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 --verify

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

Platform support

Native monitoring has two backends with the same events and output:

  • Linux reads /proc: process records, /proc/<pid>/fd socket 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 with KERN_PROCARGS2. For each target it lists socket descriptors with proc_pidinfo(PROC_PIDLISTFDS) and reads each socket's TCP/UDP endpoints and state with proc_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 inode field holds the kernel's opaque socket handle on macOS; there are no socket inodes there.
  • start_time is 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 (01 established, 0A listen, ...).
  • An IPv6 socket carrying IPv4 traffic is reported as an IPv4-mapped IPv6 address, as Linux tcp6/udp6 do.

WSL2 runs the Linux backend; targets must be visible inside that Linux environment and its network namespace.

Windows

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.

From Source

git clone https://github.com/Dicklesworthstone/rano
cd rano
cargo build --release
cp target/release/rano ~/.local/bin/

Cargo Install

cargo install --git https://github.com/Dicklesworthstone/rano --locked

Quick Start

  1. Run with defaults (auto-monitors claude/codex/gemini):
    rano
  2. Target a process name:
    rano --pattern codex
  3. Log to SQLite (default observer.sqlite):
    rano --sqlite /tmp/rano.sqlite
  4. See aggregates:
    sqlite3 /tmp/rano.sqlite "select * from provider_counts;"

Commands

rano

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 --once

Common 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-config

Domain Attribution Modes

rano 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 pcap and 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 ptr

Offline 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-banner

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

rano update

Update the installed binary.

rano update
rano update --version v0.1.0
rano update --system
rano update --owner your-org --repo rano --branch main

rano report

Generate 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 always

Report 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 unknown groups
  • 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.

rano diff

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

rano status

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

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


Alert Thresholds

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.

Quick Examples

# 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

Alert Flags

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.

Alert Output Format

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"}

SQLite Alert Tracking

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';

Use Cases

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 50

Rate limit detection: Identify when AI CLI tools are making too many connections.

rano --alert-max-per-provider 100 \
     --alert-duration-ms 60000

Compliance monitoring: Log all connections that match suspicious patterns.

rano --alert-domain "*.cn" \
     --alert-domain "*.ru" \
     --sqlite /var/log/rano-audit.sqlite

Troubleshooting

Alerts aren't firing

  1. Verify the pattern matches: rano --alert-domain "*.example.com" --once should show the flag is accepted
  2. Check cooldown: default is 10 seconds. Use --alert-cooldown-ms 1000 for more frequent alerts
  3. Ensure --no-alerts is not set
  4. For domain alerts, ensure hostname evidence is available (--no-dns disables PTR only; pcap may still supply DNS/SNI)

Too many alerts (spam)

  1. Increase cooldown: --alert-cooldown-ms 60000 (60 seconds)
  2. Narrow patterns: use more specific domain patterns
  3. Raise thresholds: increase --alert-max-connections value

Performance

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.


Retry Detection

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 --json mode 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_count in SQLite (events.retry_count) for post-hoc queries.

Secrets & Privacy

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 paranoia

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

Presets

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.

Built-in Presets

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

Quick Examples

# 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

Preset Flags

Flag Description
--preset <name> Load a preset configuration (repeatable)
--list-presets Show all available presets and exit

Creating Custom Presets

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
EOF

The first line comment # Description: ... becomes the preset description shown in --list-presets.

Preset file format:

  • One key=value per line
  • Lines starting with # are comments
  • Uses the same keys as config files (see Configuration section)
  • Boolean values: true/false, yes/no, on/off, or 1/0

Preset Merging

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 verbose

CLI 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 5000

Troubleshooting

Preset not found

error: Unknown preset 'mypreset'. Available: audit, live, quiet, verbose
  1. Check the preset name spelling
  2. Ensure the preset file exists: ls ~/.config/rano/presets/
  3. Verify file extension is .conf
  4. Run rano --list-presets to see available presets

Preset not loading values

  1. Check file format: must be key=value; whitespace around the key and value is trimmed
  2. Verify key names match config options (see Configuration section)
  3. For boolean values, use true/false, yes/no, on/off, or 1/0

Process Ancestry

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.

How It Works

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.

Display Format

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)

SQLite Storage

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%';

Export Support

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"))'

Use Cases

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.

Performance

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.


Exporting Data

rano can export SQLite event history to CSV or JSONL for use with external tools like Excel, Pandas, jq, or data pipelines.

Quick Examples

# 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

Export Flags

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

Output Fields

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

Format Notes

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 jq pipelines

Use Cases

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 -u

Count connections per domain

rano export --format jsonl | jq -r '.domain // "unknown"' | sort | uniq -c | sort -rn | head -20

Import to Pandas

import pandas as pd
df = pd.read_csv("connections.csv")
df.groupby("provider")["event"].count()

Configuration Validation

rano provides built-in configuration validation to catch errors and typos before runtime. The config subcommand helps you verify, inspect, and troubleshoot your configuration.

Quick Examples

# 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

Config Subcommands

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.

Validation Output

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?)

What Gets Validated

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

Workflow

  1. Create or edit config:

    vim ~/.config/rano/config.conf
  2. Validate before running:

    rano config check
  3. If errors, fix and re-validate:

    1 error(s) found:
      ✗ config.conf:3: 'json' must be a boolean...
    
  4. Once valid, run rano:

    rano --pattern claude

Common Issues and Fixes

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)

E2E Testing

Config validation is tested via the E2E harness:

scripts/e2e/run.sh config-validation scripts/e2e/config-validation.sh

Logs are written to logs/e2e/config-validation-*.log.


Configuration

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=vivid

SQLite Batching Performance

SQLite 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_size to 500-1000, increase db_queue_max to 50000
  • Low latency queries: Decrease db_flush_ms to 100-500
  • Memory constrained: Decrease db_queue_max to 1000-5000
  • SSD storage: Defaults work well
  • HDD storage: Increase db_batch_size to 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.

Provider Configuration (rano.toml)

Customize provider pattern matching without recompiling. rano searches for config at:

  1. ~/.rano.toml
  2. ~/.config/rano/rano.toml
  3. ./rano.toml
  4. --config-toml <path> (CLI)
  5. 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
google 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

Architecture

┌──────────────────────────────┐
│        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 │
└──────────────┘  └────────────────┘

Troubleshooting

"warning: pcap capture requires elevated privileges"

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 ptr

"no matching processes"

Your patterns didn’t match any running processes. List processes or add --pid.

ps aux | rg -i codex
rano --pid <pid>

"SQLite file is locked"

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).sqlite

"domains are unknown"

PTR lookups are disabled or reverse DNS doesn’t resolve for that IP.

Edit your config:

no_dns=false

"stats aren't printing"

Stats are suppressed when JSON output is enabled or when --stats-interval-ms 0.

rano --stats-interval-ms 2000

Live Stats Views

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

Available Views

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

Quick Examples

# 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

Stats Flags

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

Output Examples

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

E2E Testing

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-*.log

The test verifies that all four view types render correctly and cycle in sequence.

Configuration

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=5000

Or via presets:

# The 'live' preset enables frequent stats
rano --preset live  # stats_interval_ms=500, stats_top=10; provider/domain cycle every 5000 ms

Verify pcap attribution (E2E)

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

Logs:

  • logs/e2e/pcap-attribution-<timestamp>.log
  • logs/e2e/outputs/ for full command output

Limitations

What rano Doesn’t Do (Yet)

  • 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 /proc or 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.

FAQ

Why “rano”?

Short for rust_agent_network_observer.

Does it require root?

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.

Can I monitor a single PID without descendants?

rano --pid 1234 --no-descendants

How do I get JSON output for automation?

rano --json --log-file /tmp/rano.jsonl

Where are logs stored?

--log-file writes a single file; --log-dir creates per-run files. SQLite logs default to observer.sqlite.


About Contributions

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 gh and 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.


License

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.

About

Network observer that tracks outbound connections from AI CLI processes (Claude Code, Codex, Gemini), attributing sockets to providers in real time with SQLite logging

Topics

Resources

Stars

31 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages