AI coding agents run shell commands, open files, and make network calls on
your machine, and most of it goes unseen. agentcap records that activity from
the kernel with eBPF and exposes it as per-agent Prometheus metrics with a
Grafana dashboard. It needs no SDK or changes to the agents, works on agents
that are already running, and attributes child processes (a bash or curl a
tool spawns) to the agent that launched them.
Per agent, it reports:
- tools run — each binary the agent exec'd
- domains queried and destination ports
- files opened, read vs. write
- CPU time, network bytes, file I/O, and process churn
That's enough to audit what an agent did, notice an unexpected domain or port, or track resource use over time. OpenClaw, Claude Code, Codex, Gemini, aider and others are recognized by default; add any process by name. Built on yeet and eBPF.
curl -fsSL https://yeet.cx | sh # 1. install yeet (CLI + yeetd daemon)
yeet login # 2. authenticate this host
git clone https://github.com/yeet-src/agentcap && cd agentcap
make up # 3. build + start the exporter on :9464
make wire # 4. print how to add it to your Grafanamake up runs the exporter (a Prometheus endpoint at 127.0.0.1:9464);
make wire prints the scrape-config + dashboard-import steps for your Grafana.
No Grafana handy? Run make demo for a throwaway Prometheus + Grafana in
Docker at http://localhost:3000/d/agentcap/agent-activity. Full detail in
Setup; make down stops everything.
Tracked out of the box (edit src/agents.txt to change the
list — one comm prefix per line):
Each name is a process-comm prefix; a match pulls in that process's whole
tree, so the bash / node / curl an agent spawns is counted under it. Only
agents actually running appear in the metrics — the list is the watch set, not
a requirement that all be present.
The BPF side (src/bpf/agentcap.bpf.c) is policy-free: the collector
reads the prefix list from src/agents.txt and pushes it into a kernel
LPM trie at startup, so matching is a single loop-free trie lookup with
no per-agent cost. The word-boundary rule is encoded in the data — each prefix
is inserted as name\0 / name- / name_ / name. — so "pi" catches
pi, not pipewire. A task is tracked when its comm matches the trie or
when a tracked task forks it — each tree keeps the
identity of the prefix that rooted it, so a bash exec'd by openclaw is
counted as agent="openclaw". Roots that predate the probe (a running
gateway) are adopted on their first context switch, fork, or socket/file op.
Attach types: tracepoints, LSM hooks, and fexit trampolines — no
kprobes. LSM (socket_connect, socket_sendmsg) carries exact
before-the-fact semantics and is a stable security API; fexit supplies what
LSM never sees: actual received bytes (sock_recvmsg) and actual file I/O
(vfs_read/vfs_write). The LSM programs need a kernel with BPF-LSM
available, which modern distros ship; if it isn't, the probe reports
agentcap_probe_up 0 and the app stays up rather than crashing.
Egress is split by rate: rare lifecycle events (fork/exec/exit, with names)
stream over a ring buffer; high-rate sums (CPU ns, socket and file bytes)
live in a {agent, slot}-keyed hash the collector polls once a second.
For security review, the probe also streams, per agent, the destination port of every inet connect and the domain of every name lookup it can see. To keep the kernel side verifier-safe, the probe only filters and copies raw bytes; the collector parses them in JavaScript, where loops are free. Two lookup paths are covered:
- Port-53 DNS — direct resolver traffic (c-ares, aiodns, dig): the DNS packet head is shipped and the qname decoded from label format.
- nss-resolved varlink — glibc getaddrinfoon systemd hosts resolves over a unix socket, never touching port 53. The probe recognizes the varlinkResolveHostnameJSON on tracked tasks' unix sends and lifts the name.
DoH/DoT are invisible — an agent resolving over its own HTTPS/TLS
channel shows only as a :443 connect, not a domain. That's a real limit
of watching from the kernel, documented rather than papered over.
All labeled agent. A few carry an extra label — execs a comm (the tool
that ran), net_connections a port, dns_queries a domain, and
files_opened a path + mode — each capped per agent (then folded into
other) to bound cardinality.
yeet_worker_up is prepended by the renderer: 0 means the scrape route is
alive but the collector worker is not.
It works out of the box: the common agents — OpenClaw, Claude Code, Codex,
Gemini, aider, opencode, goose, cline, continue, cursor, qwen, crush, amp,
grok, omp, pi — are pre-filled in src/agents.txt, one
comm prefix per line. Edit that file to change what's watched.
curl -fsSL https://yeet.cx | sh # installs the yeet CLI + the yeetd daemon
yeet login # authenticate this host
yeet status # must print "Status: Ok."yeetd is the daemon the installer sets up; it does the eBPF loading and runs
the service. Verify your environment anytime with make check.
make up # build + start the exporter on 127.0.0.1:9464 (no Docker)That's the whole product — a Prometheus endpoint at
http://127.0.0.1:9464/metrics (make metrics to eyeball it). make up is
also how you pick up edits to src/ (it re-imports; the daemon copies unit
scripts at import time). Stop it with make down.
The exporter is a plain Prometheus endpoint, so add it to the Grafana +
Prometheus you already run — no Docker. make wire prints both steps with
absolute paths:
-
Prometheus — scrape the exporter. It binds 0.0.0.0:9464, so use127.0.0.1on the same host or the host's IP from a remote Prometheus (firewall the port if the box is network-reachable):scrape_configs: - job_name: agentcap static_configs: - targets: ["127.0.0.1:9464"] # or "<host-ip>:9464" from elsewhere
-
Grafana — with a Prometheus datasource in place, import deploy/grafana/dashboards/agent-activity.json(Dashboards → New → Import); pick your datasource if its uid isn'tprometheus. Re-import to update it later (regenerate first withmake dashboardif you edited the generator).
No Grafana handy?
make demospins up a throwaway Prometheus + Grafana in Docker with the dashboard pre-loaded, at http://localhost:3000/d/agentcap/agent-activity.make downtears it back down. Docker is only needed for this.
The service (service.toml) is three units: keeper (eager) holds the
collector shared worker — and the BPF probe and metric registry inside it —
alive; scrape (lazy, per-connection) renders one exposition document per
request over the console portal; web binds 0.0.0.0:9464 and mounts
/metrics.
Edit src/agents.txt — one comm prefix per line, #
comments and blank lines ignored — then make up. Or override for one
run without editing anything:
make dev AGENTS=openclaw,claude,mybot # standalone, live dump, no HTTPLower-level pieces make up/demo build on: make deploy (import + start the
service), make obs-up / make obs-down (just the Docker stack), and
make start / stop / restart / remove (service lifecycle).
The Agent Activity dashboard (deploy/grafana/dashboards/agent-activity.json)
is filterable by agent, with a stable color per agent across every panel. It
has four sections:
- Overview — hero stats, tracked tasks, tool-exec rates, a "which agent ran what" table, top tools, CPU / network / file-I/O / churn timeseries.
- Audit — who talked to what — per-agent domain and destination-port tables (ports classified and color-coded: HTTPS/DNS green, SSH/SMTP amber, telnet/RDP red, unknown neutral), plus DNS and connection rates.
- Activity mix — CPU-share and egress-share donuts, a tool-launch bar chart, an active/idle state-timeline across agents, and per-agent CPU gauges — varied forms for a fast read.
- Agent detail — $agent — a repeated, collapsed row per selected agent with its own tools, lifecycle, CPU/tasks, network, domains and ports.
The dashboard is generated by deploy/grafana/gen-dashboard.py (edit the
Python, rerun it, Grafana's file provider reloads within 30s) so the panel
boilerplate and the per-agent color mapping stay consistent.
- .github/workflows/ci.yml— BPF object builds, JS syntax,- service.tomlparses,- promtool check config, dashboard JSON lint, compose config.
- .github/workflows/kernel-matrix.yml— boots 6.1 / 6.6 / 6.12 / bpf-next in VMs and runs veristat against- bin/probe.bpf.o; a rejection on an old kernel is the signal of the minimum supported kernel (LSM + fexit programs need BTF and- CONFIG_BPF_LSM). Same check locally:- make veristat-matrix.
src/bpf/agentcap.bpf.c the probe: sched tracepoints + LSM + fexit
src/agents.txt the tracked agent list (one comm prefix per line)
src/agents.js parses agents.txt into the prefix array
src/collector.js shared worker: fills kernel maps, owns the
Telemetry registry, polls counters, serve()s scrapes
src/scrape.js per-request /metrics renderer (console portal)
src/main.js eager keeper — holds the worker (and probe) alive
service.toml yeet service: units, web server, /metrics route
deploy/ prometheus.yml, docker-compose, grafana provisioning
Makefile build + run lifecycle (make up, wire, demo, …)
The whole thing is a small yeet script — a BPF program (src/bpf/*.bpf.c), a
JS collector (src/collector.js), and a generated dashboard
(deploy/grafana/gen-dashboard.py). Each of these is a one- or two-file
change, so point a coding agent at this repo and paste a prompt:
Watch a new agent
Add
mybotas a new line insrc/agents.txtand redeploy withmake up. Then scrapehttp://127.0.0.1:9464/metricsand confirmagentcap_tasks{agent="mybot"}shows up while mybot is running. Leave the other agents' matching unchanged.
Alert on suspicious egress
Add a Grafana alert rule that fires when
agentcap_net_connections_totalrecords a destination port outside {80, 443, 53} for any agent. Include theagentandportlabels in the notification text. Add the rule to the generated dashboard so it ships with the repo.
Map where agents connect (geomap)
Extend the BPF
conn_eventto include the destination IP, decode it insrc/collector.js, and add a GeoIP lookup so each connect gets a country. Expose it asagentcap_connections_by_country_total{agent,country}and add a Grafana geomap panel indeploy/grafana/gen-dashboard.py. This should also surface DoH/DoT egress that has no visible domain.
Add a metric
Add a gauge
agentcap_agents_totalinsrc/collector.jscounting agents with at least one live task, updated on the existing poll loop. Register it in the telemetry registry next to the others. Then add a stat tile for it in the dashboard generator and rerunmake dashboard.
Restrict what's watched
Change the probe so it only tracks agents whose process is under a given systemd unit or cgroup, and add that as a metric label. Keep the comm-prefix matching from
src/agents.txtas a second filter. Document the new option in the README's Setup section.
Ship it somewhere else
Add a second scrape route to
service.tomlthat renders the registry as OpenMetrics or JSON instead of Prometheus text. Reuse the shared collector worker so it reports the same data. Updatemake wireto mention the new endpoint.
Built with yeet, a JS runtime for writing eBPF programs and live system dashboards on Linux. Join us on Discord.