A scrolling audit log of everything a Claude Code session does:
every command it execs (with the full argv), every file it opens, and
every TCP port it reaches out to or binds — for the matched session and
everything beneath it, and nothing else.
Where its sibling claudetree paints who is running as
a live process tree, claudefeed is the companion the claudetree README
promised: the tail -f of what they did.
claudefeed — auditing "claude" sessions · seeded 9 processes from 288 live · streaming exec/exit/open/conn/listen …
16:11:22.023 3003897 exit sleep exit 0
16:11:22.023 3003924 exec uname · /usr/bin/uname -a
16:11:22.024 3003924 exit uname exit 0 after 1ms
16:11:22.024 3003925 exec curl · curl -s -m 3 http://example.com
16:11:22.033 3003925 conn curl → 104.20.23.154:80
16:11:22.053 3003925 exit curl exit 0 after 29ms
Each line is time · pid · class · detail. The pid and that process's
program name share a stable per-process color, so a burst of activity
reads as one actor; re-exec of the same pid keeps its color. Within a
command line and an opened path, flags are muted and paths are cyan, so
a bash -c '…' pipeline is legible at a glance. The class is a color-keyed
badge:
| class | source | detail |
|---|---|---|
exec |
sys_enter_execve |
program name + full command line |
exit |
sched/sched_process_exit |
exit N / killed sig N, + lifetime |
open |
sys_enter_openat |
(mode) + path |
conn |
kprobe/tcp_connect |
→ addr:port (outbound) |
listen |
kprobe/inet_listen |
bound addr:port |
A system-wide feed of every openat would be a firehose. The filtering
happens in the kernel: a tracked hash map holds the tgid of every
process that belongs to a session, and the file/network probes are no-ops
for any pid not in it — so the noise never crosses into userspace. The set
stays current from three directions:
- JS seeds it. One
yeet.graphquery at startup finds the live session processes and all their descendants and writes the tgids intotrackedvia the map API (updateBatch). execself-propagates it. When a tracked process exec's a child, the child inherits membership. A fresh session — one whose parent isn't tracked — is caught by matching the exec'd program's basename against a needle the JS side patches into the program's config.exitprunes it. The thread-group leader leaving drops its tgid.
A start hash map keyed by tgid bridges exec→exit so the exit line
can report how long the process lived (processes already alive at attach
have no stamp and report an unknown lifetime).
makeDumps the running kernel's BTF to include/vmlinux.h (needed for the
tracepoint context structs, task_struct, and the sock/socket
structs the network kprobes read), then compiles claudefeed.bpf.c.
Requires clang, bpftool, and a kernel with BTF.
If your kernel's BTF is newer than the bundled libbpf headers, clang may flag a conflicting
bpf_stream_vprintkdeclaration. Build against the system libbpf instead:make LIBBPF_INCLUDE=/usr/include.
yeet run .Args (both --match=x and the bare match=x form work):
| arg | default | meaning |
|---|---|---|
match=<name> |
claude |
session program-name needle (see below) |
secs=<n> |
0 |
run for N seconds, 0 = until Ctrl-C |
only=<classes> |
all | show only these, e.g. only=exec,conn,listen |
no=<classes> |
none | drop these, e.g. no=open (the noisiest class) |
yeet run . no=open secs=30 # commands + network only, for 30s
yeet run . only=exec,conn # just "what ran" and "what it dialed"
yeet run . match=node # audit node sessions insteadmatch is tested against each exec'd program's basename (a prefix
match, case-insensitive) and, for the startup seed, against comm or
argv[0]'s basename — never the whole command line, so a process that
merely mentions the name in a path or argument doesn't masquerade as a
session. The same needle drives both the JS seed and the kernel-side
catch of fresh sessions, so the two agree on what counts.
Colors come from yeet's style global (standard 16-color palette, no
truecolor), which no-ops to plain text when stdout isn't a TTY — so a
piped run is a clean plain-text log you can grep or archive.
| file | responsibility |
|---|---|
main.js |
wiring: bind/start, push the needle, seed tracked, stream the ring |
config.js |
yeet.args → constants (match, secs, only/no, event kinds) |
model.js |
seed the session subtree from the sysgraph; decode record fields |
feed.js |
format one record into a colored audit line |
yeet.graph.query(gql)resolves a GraphQL query against the sysgraph; hereprocs { pid stat { ppid comm } cmdline }seeds the membership set.- Programs are auto-attached on
start()by theirSEC()name: three tracepoints (tp/syscalls/*,tp/sched/*) and two kprobes (kprobe/tcp_connect,kprobe/inet_listen). HashMap("tracked")is written from JS to seed membership; the kernel maintains it thereafter.DataSecpatches the match needle into the program's.datasection at runtime.eventsis a ring buffer bound by itsbtf_struct(event);RingBuf.subscribestreams each record back as a decoded object. Achar[]field arrives as a JS string and an__u8[N]field (the raw address) as a byte object, so argv is space-joined kernel-side into onechar[]while addresses stay binary-clean.