{"id":51002099,"url":"https://github.com/yeet-src/claudefeed","last_synced_at":"2026-06-20T15:33:09.198Z","repository":{"id":363674447,"uuid":"1256556655","full_name":"yeet-src/claudefeed","owner":"yeet-src","description":"Live audit log of every command, file, and network connection a Claude Code (or any matched) session makes, from the kernel.","archived":false,"fork":false,"pushed_at":"2026-06-09T22:35:40.000Z","size":6148,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-09T23:13:48.314Z","etag":null,"topics":["ai-agents","audit","bpf","ebpf","kernel","kprobe","linux","llm","observability","provenance","security","tracepoint","tracing","yeet"],"latest_commit_sha":null,"homepage":"https://yeet.cx","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yeet-src.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T22:22:28.000Z","updated_at":"2026-06-09T22:47:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yeet-src/claudefeed","commit_stats":null,"previous_names":["yeet-src/claudefeed"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/yeet-src/claudefeed","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fclaudefeed","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fclaudefeed/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fclaudefeed/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fclaudefeed/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeet-src","download_url":"https://codeload.github.com/yeet-src/claudefeed/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fclaudefeed/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34576042,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","audit","bpf","ebpf","kernel","kprobe","linux","llm","observability","provenance","security","tracepoint","tracing","yeet"],"created_at":"2026-06-20T15:33:09.123Z","updated_at":"2026-06-20T15:33:09.183Z","avatar_url":"https://github.com/yeet-src.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `claudefeed`\n\n\u003e **`tail -f` for Claude Code.** Every command a session runs, every file it opens, every TCP port it reaches or binds — decoded and streamed live to your terminal. Scoped to that session's process subtree. No other PIDs, no noise.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/platform-Linux-1793D1\" alt=\"Linux\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/built%20with-yeet%20%2B%20eBPF-8A2BE2\" alt=\"yeet + eBPF\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-GPL--2.0-3DA639\" alt=\"GPL-2.0\"\u003e\n  \u003ca href=\"https://discord.gg/dYZu9PjKB\"\u003e\u003cimg src=\"https://img.shields.io/badge/chat-Discord-5865F2\" alt=\"Discord\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n![claudefeed demo](assets/claudefeed.gif)\n\n**`claudefeed` turns a Claude Code session into a live, decoded audit log: every `exec` (with full argv), every `openat`, and every outbound or bound TCP port — for the matched session and its entire subtree.**\n\nWhere its sibling [`claudetree`](https://github.com/yeet-src/claudetree) paints *who is running* as a live process tree, `claudefeed` is the companion that tree promised: the `tail -f` of *what they did*.\n\n\u003e [!TIP]\n\u003e **You can't just `strace` this.** A session is a moving tree of processes — Claude spawns a shell, the shell spawns `git`, `git` spawns `ssh`. A system-wide feed of every `openat` would be a firehose, and a per-PID trace misses every child. `claudefeed` filters **in the kernel**: a `tracked` set of session tgids gates the probes, the set self-propagates across `exec`, and the noise never crosses into userspace.\n\n## Quick start\n\n```sh\ncurl -fsSL https://yeet.cx | sh\nyeet run github:yeet-src/claudefeed\n```\n[Manual install guide](https://yeet.cx/docs/manual-installation) | Linux only\n\nWith a Claude Code session running anywhere on the box, that's it — `claudefeed` finds the live `claude` processes, seeds its tracked set, and starts streaming. Everything after `--` is passed to claudefeed:\n\n| flag               | default  | meaning                                                          |\n| ------------------ | -------- | ---------------------------------------------------------------- |\n| `--match=\u003cname\u003e`   | `claude` | session program-name needle; matched against the exec'd basename (case-insensitive prefix) |\n| `--secs=\u003cn\u003e`       | `0`      | run for N seconds, `0` = until Ctrl-C                            |\n| `--only=\u003cclasses\u003e`   | all      | show only these classes, e.g. `--only=exec,conn,listen`        |\n| `--except=\u003cclasses\u003e` | none     | drop these classes, e.g. `--except=open` (the noisiest)        |\n\n```sh\nyeet run github:yeet-src/claudefeed -- --except=open --secs=30   # commands + network only, 30s\nyeet run github:yeet-src/claudefeed -- --only=exec,conn      # just \"what ran\" and \"what it dialed\"\nyeet run github:yeet-src/claudefeed -- --match=node          # audit node sessions instead\n```\n\n## A 60-second primer on the `tracked` set\n\nThe hard part of auditing a session isn't capturing events — it's capturing *only* the session's events, as its process tree grows and shrinks. `claudefeed` keeps a `tracked` hash map of session tgids in the kernel; the file and network probes are no-ops for any pid not in it. The set stays current from three directions:\n\n**1. JS seeds it.** One `yeet.graph` query at startup finds the live session processes and all their descendants and writes the tgids into `tracked` in a single `updateBatch` syscall — so activity from processes already running when you attach is captured too.\n\n**2. `exec` self-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 `.data` section at runtime. The same needle drives both the seed and the kernel-side catch, so the two always agree on what counts.\n\n**3. `exit` prunes it.** The thread-group leader leaving drops its tgid. A separate `start` 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).\n\nBecause `match` is tested against the program **basename** only — never the whole command line — a process that merely mentions `claude` in a path or argument doesn't masquerade as a session.\n\n## Common use cases\n\n`claudefeed` is for anyone running Claude Code (or any agent) with real autonomy and wanting a ground-truth record of what it actually did — not what it said it did.\n\n- An autonomous agent run finished. What commands did it actually execute, and in what order?\n- A session reached out to a host or opened a file it had no business touching. Catch it on the wire.\n- A long unattended run needs a tripwire — get paged the moment it does something notable (see [Alerting](#alerting)).\n- You want a clean, grep-able, archivable log of a session for forensics or review.\n\n## What you're looking at\n\n```\nclaudefeed — auditing \"claude\" sessions · seeded 9 processes from 288 live · streaming exec/exit/open/conn/listen …\n16:11:22.023  3003897  exit    sleep exit 0\n16:11:22.023  3003924  exec    uname · /usr/bin/uname -a\n16:11:22.024  3003924  exit    uname exit 0 after 1ms\n16:11:22.024  3003925  exec    curl · curl -s -m 3 http://example.com\n16:11:22.033  3003925  conn    curl → 104.20.23.154:80\n16:11:22.053  3003925  exit    curl exit 0 after 29ms\n```\n\nEach 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:\n\n| class    | source                     | detail                                |\n| -------- | -------------------------- | ------------------------------------- |\n| `exec`   | `sys_enter_execve`         | program name + full command line      |\n| `exit`   | `sched/sched_process_exit` | `exit N` / `killed sig N`, + lifetime |\n| `open`   | `sys_enter_openat`         | `(mode)` + path                       |\n| `conn`   | `kprobe/tcp_connect`       | `→ addr:port` (outbound)              |\n| `listen` | `kprobe/inet_listen`       | bound `addr:port`                     |\n\nColors 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.\n\n## How it works\n\nThe core is in [`claudefeed.bpf.c`](claudefeed.bpf.c) and [`main.js`](main.js).\n\n### The BPF side\n\nOne BPF object attaches three tracepoints and two kprobes, auto-attached on `start()` by their `SEC()` names:\n\n| Program | Hook | What it does |\n|---|---|---|\n| `exec`   | `tp/syscalls/sys_enter_execve` | Emit the program + full argv; catch fresh sessions by basename, propagate membership to children. |\n| `exit`   | `tp/sched/sched_process_exit`  | Emit exit status + lifetime; prune the leaving tgid from `tracked`. |\n| `open`   | `tp/syscalls/sys_enter_openat` | Emit `(mode)` + path — only for tracked pids. |\n| `conn`   | `kprobe/tcp_connect`           | Emit the outbound `addr:port`. |\n| `listen` | `kprobe/inet_listen`           | Emit the bound `addr:port`. |\n\nThree maps connect kernel to userspace:\n\n- `tracked` — `HASH` of session tgids. Seeded from JS, maintained by the kernel thereafter; gates every file/network probe.\n- `start` — `HASH` keyed by tgid, bridging `exec`→`exit` so the exit line can report a lifetime.\n- `events` — `RINGBUF` bound by its `btf_struct` (`event`), one decoded record per event.\n\n### The JS side\n\n| file | responsibility |\n|---|---|\n| `main.js`   | wiring: bind/start, patch the needle, seed `tracked`, stream the ring |\n| `config.js` | `yeet.args` → constants (`match`, `secs`, `only`/`except`, event kinds) |\n| `model.js`  | seed the session subtree from the sysgraph; decode record fields |\n| `feed.js`   | format one record into a colored audit line |\n\n`RingBuf.subscribe` streams each record back as a decoded object: a `char[]` 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 one `char[]` while addresses stay binary-clean.\n\n### Why tracepoints and kprobes, not a syscall wrapper\n\nThe membership trick needs to fire *inside* `exec` (to propagate to children) and *inside* `exit` (to prune), at the moment the kernel does them — for the whole tree at once, with no PID named on the command line. Tracepoints and kprobes give exactly that, and the in-kernel `tracked` gate means the firehose never reaches userspace.\n\n## Alerting\n\nThe feed is local — but the same ring-buffer callback that prints each line can also *page you*. yeet posts to Slack: log in at [yeet.cx/settings](https://yeet.cx/settings), connect your workspace once, and `yeet.alert` can then send to any channel it can reach.\n\nThe subscribe callback in `main.js` already holds every decoded record, so an alert is just a branch inside it. To know the moment a Claude session dials out to a host it has no business touching:\n\n```js\n// pull in the same decoders feed.js uses\nimport { cstr, fmtAddr } from \"./model.js\";\n\nconst WATCH = \"yeet.cx\"; // an IP, a port, or a command needle work too\n\nconst sub = await ring.subscribe(async (rec) =\u003e {\n  const e = rec.event ?? rec;\n  stats.events++;\n  if (!SHOW.has(KIND_KEY[e.kind])) return;\n  console.log(line(e, Date.now()));\n\n  if (e.kind === EVT_CONNECT) {\n    const peer = fmtAddr(e.family, e.addr, e.port);\n    if (peer.includes(WATCH)) {\n      await yeet.alert({\n        method: \"slack\",\n        channel: \"#alerts\",\n        text: `claude (pid ${e.pid}) connected to ${peer}`,\n        blocks: [\n          { type: \"header\", text: { type: \"plain_text\", text: \"Claude phoned home\" } },\n          {\n            type: \"section\",\n            fields: [\n              { type: \"mrkdwn\", text: `*Process:*\\n${cstr(e.comm)} (${e.pid})` },\n              { type: \"mrkdwn\", text: `*Peer:*\\n${peer}` },\n            ],\n          },\n        ],\n      });\n    }\n  }\n});\n```\n\nThe same shape fits any class: alert on an `EVT_EXEC` whose `cstr(e.cmdline)` matches `rm -rf` or `curl … | sh`, or an `EVT_OPEN` that touches `~/.ssh/id_*`. The callback already has the decoded record in hand — you only add the predicate and the `yeet.alert`.\n\n## Requirements\n\n\u003e [!IMPORTANT]\n\u003e A Linux kernel with **BTF** — needed for the tracepoint context structs, `task_struct`, and the `sock`/`socket` structs the network kprobes read. Default on current Arch, Fedora, Ubuntu, and Debian.\n\u003e\n\u003e The yeet daemon, which handles the privileged BPF load. `curl -fsSL https://yeet.cx | sh` installs it.\n\n## Honest caveats\n\n\u003e [!NOTE]\n\u003e claudefeed is observability, not enforcement. It tells you what happened; it does not stop anything from happening.\n\n- claudefeed is an audit log. It reports what a session did; it does not block or sandbox anything. (to block actions, [contact us](https://yeet.cx/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=claudefeed\u0026utm_content=caveats-block))\n- It records that a file was opened, not what was read or written. `open` events come from `openat` only; the older `open` syscall and memory-mapped or other I/O paths are not shown. ([contact us](https://yeet.cx/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=claudefeed\u0026utm_content=caveats-open) for custom yeet scripts)\n\n## Community questions\n\n**Why not just read Claude's own logs?**\nThose tell you what Claude *thinks* it did. `claudefeed` reads the kernel, so it records what actually crossed `execve`, `openat`, and the TCP stack — including everything the spawned shells, `git`, and subprocesses did on their own.\n\n**Does it slow the session down?**\nNo meaningful overhead. The probes are passive observers and the in-kernel `tracked` gate drops non-session events before they ever cost a ring-buffer write.\n\n**Will it catch a session I start *after* attaching?**\nYes. A fresh session is caught the moment it exec's a program whose basename matches the needle — that's the kernel-side half of the membership trick.\n\n**Does it only work for Claude?**\nNo. `--match` is just a program-name needle. `--match=node`, `--match=python`, `--match=bash` — anything that exec's under a recognizable name works the same way.\n\n**Can I export the data stream?**\nNot built in today. The feed renders to stdout (or plain text when piped), so the quick path is to run claudefeed with whatever `--only=`/`--except=` filter you want and tee the output into a log file or your shipper of choice. For a structured export, say JSON over HTTP, a Kafka topic, an S3 sink, or a SIEM pipeline, the `ring.subscribe` callback in `main.js` is where you'd add it; the same shape as the Slack alerting example above works for any sink. To set up a managed export pipeline, [contact us](https://yeet.cx/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=claudefeed\u0026utm_content=faq-export).\n\n## Building from source\n\n```sh\nmake          # dumps vmlinux.h via bpftool, builds claudefeed.bpf.o\n```\n\nNeeds `clang` (BPF target) and `bpftool`, plus a kernel with BTF. The generated `include/vmlinux.h` and `*.bpf.o` are build artifacts.\n\n## License\n\nGPL-2.0. The BPF program declares `char LICENSE[] SEC(\"license\") = \"GPL\"` in [`claudefeed.bpf.c`](claudefeed.bpf.c), required for the kernel helpers it uses.\n\n---\n\nBuilt with [yeet](https://yeet.cx/docs/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=claudefeed), a JS runtime for writing eBPF programs on Linux machines. Join us on [discord](https://discord.gg/dYZu9PjKB?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=claudefeed).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fclaudefeed","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeet-src%2Fclaudefeed","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fclaudefeed/lists"}