Attach to the running Claude Code sessions and render their live
process trees in the terminal — every shell, MCP server, node, and
build tool the session spawns, updated as it happens.
It combines the two halves of yeet's observability surface:
yeet:graph— the snapshot. One GraphQL query against the sysgraph at startup seeds the full process table (pid → ppid,comm,cmdline). That is the known snapshot: everything already alive when we attached, with the parent links needed to build a tree.yeet:bpf— the deltas. Twoschedtracepoints (sched_process_exec/sched_process_exit) stream every birth and death afterwards over a ring buffer. Each event is applied to the table as a diff, so the tree stays current without re-polling/proc.
We track the whole system table but only render the subtrees rooted at a
matching session, so a process that execs three levels deep still slots
into the right place. A process that just spawned flashes green with a +
for a few seconds; one that just exited lingers as a dim strikethrough line
(with its exit status) before dropping out.
Each node shows its PID and comm, pstree-style. PIDs are tinted by a hash
of the pid, so the same process keeps a stable color you can track down the
tree; the matched session command is bold cyan.
claudetree — live process trees matching "claude" · 286 procs · 41 events · 137s
+ just spawned · strikethrough = just exited · cyan = session match · pid color = per-process
2992335 claude
├─ 2992767 clangd.main
└─ 2994417 zsh
├─ 2996569 sleep exit 0 after 1.0s
└─ + 2996573 node
(For a scrolling feed of the full exec command lines, see the companion exec-feed script — this one stays focused on the tree.)
makeDumps the running kernel's BTF to include/vmlinux.h (needed for the
sched tracepoint context structs and task_struct), then compiles
claudetree.bpf.c. Requires clang, bpftool, and a kernel with BTF
(/sys/kernel/btf/vmlinux).
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 .Runs until Ctrl-C. Options are passed as yeet.args:
| arg | default | meaning |
|---|---|---|
match=<name> |
claude |
command name to anchor a session on |
secs=<n> |
0 |
stop after n seconds (0 = until Ctrl-C) |
ms=<n> |
500 |
re-render interval |
yeet run . match=node secs=30 # watch every `node` tree for 30sThe view tracks the terminal size (tty.size() + the tty resize
event): lines are clipped to the current width, and if the tree is taller
than the window it shows what fits plus a … N more lines note. Resize
the terminal and it repaints to the new geometry immediately.
The cursor is hidden while running; yeet's PTY bridge restores it automatically when the script exits.
match is tested against each process's command name — its comm
or the basename of argv[0] — not the whole command line, so a process
that merely mentions the name in a path or argument doesn't masquerade
as a session.
| file | responsibility |
|---|---|
main.js |
wiring: bpf bind/start, ring subscribe, resize, tick, teardown |
config.js |
yeet.args → constants (match, secs, ms, timings, events) |
model.js |
process table, snapshot, exec/exit deltas, session matching |
tree.js |
rendering — colored, terminal-fitted tree (uses the style global) |
term.js |
control sequences + width-aware string helpers |
Colors come from yeet's style global, which no-ops to plain text when
stdout isn't a TTY, so piped runs stay clean.
yeet.graph.query(gql)resolves a GraphQL query against the sysgraph and returns{ data: { … } }. Here:procs { pid stat { ppid comm state } cmdline }.- The
tp/sched/*programs are attached onstart();eventsis a ring buffer bound by itsbtf_struct(event), andRingBuf.subscribestreams each record back as a decoded object. - A
starthash map keyed by tgid bridges exec→exit so EXIT events can report how long the process lived.