
Founding engineer at yeet, working on kernel-side observability and the tooling around it. I write about eBPF, Linux internals, and why your telemetry bill looks the way it does.
To log every process a build starts on Linux, hook the
sched_process_exectracepoint rather than reading your build's own output, because the tracepoint carries the arguments the kernel was actually handed instead of the ones a script said it would use. The capture is the easy half. A nine-file C build emits 27 compiler invocations, a loop callingechoanddateemits 7,745 lines, and a flat stream at that volume hides the one command you were looking for. Three routes handle it differently:execsnoopfrom bcc prints one greppable line per exec host-wide,strace -ffollows forks at ptrace cost for one process, andexectopscopes to one process tree and folds repeated commands into one row per kind so the outliers rank on top.
A green pipeline and a wrong artifact is the shape this problem usually arrives in, and the fact somebody needs is always the same one: which programs actually ran between git push and the image landing in the registry. Every tool in reach answers a neighbouring question. CI logs describe what the YAML asked for, docker history describes the layers, terraform plan describes the declaration, and none of them records the exec. My own standing here is narrow. I build kernel-side tooling and watch engineers get stuck on that step; I have never owned a build farm or been on the hook for a release train.
Eight routes get used for this, and they split on two questions rather than one. The first is scope: can you point the tool at one build and have it follow that build's grandchildren, or does it hand you the whole machine and leave you grepping. The second is volume: once you have every exec, does the tool do anything about the fact that most of them are the same three commands repeated. Most of the tooling answers the first question badly and the second not at all.
| tool | can scope to one process tree | follows grandchildren | does anything about volume | records intent or execution | needs |
|---|---|---|---|---|---|
exectop (yeet) | yes, by launch, container or pid | yes, membership propagates at fork | folds repeats into one row per kind, ranks outliers | execution | BTF kernel, the yeet daemon |
bcc execsnoop | parent pid only (-P) | no | no, one line per event | execution | bcc, root |
bpftrace | with a hand-written predicate | only if you write the fork bookkeeping | whatever you aggregate in the script | execution | bpftrace, root |
strace -f | yes, one process and its forks | yes | no, and it prints every syscall too | execution | ptrace, the target restarted or attached |
auditd | filter fields including ppid | no | no, it writes a durable log | execution | root, log storage |
set -x, make --trace, Actions debug logging | per shell or per job | only where every layer opts in | no | intent | a change to the build |
pstree, ps -ejH | yes, rooted at a pid | yes, if they are still alive | no, it is a snapshot | neither, it is state | nothing |
Socket.dev, Snyk, npm audit | not applicable | not applicable | not applicable | intent, before it runs | a manifest |
exectop is an eBPF process-launch monitor for Linux: it shows every program one application starts, folds the repetition into one row per kind of command, and ranks anything that does not look like ordinary work above the rest. It loads three tracepoints, sched_process_exec for the event itself, sched_process_fork to grow the watched set, and sched_process_exit to shrink it, which is what turns "this pid" into "this application" without any bookkeeping in userspace. You name the target by launching it (./bin/exectop -- npm ci), by container (yeet run . -- --container api), or by pid (yeet run . -- --pid 4242), and the scope section of the README is explicit that those three modes promise different things.
The fold is the part that has no equivalent in the other routes. Rather than a line per event, exectop keys each exec on the command plus its flag names, drops positional paths and deduplicates repeated flags, so cc1 -quiet a.c and cc1 -quiet b.c collapse into one row carrying a count while cc1 -O2 stays separate. A demo workload that produced 7,745 lines of exec log reads as eight rows once the repetition is grouped. It runs in a terminal and refuses to start without one, which is a real constraint in CI, and the headless path exists for exactly that reason (see below). It is built on yeet, a JavaScript runtime for Linux ops, so the folding, the buckets and the outlier scoring are JavaScript you can edit rather than a fixed feature list compiled into a binary.
execsnoop is the tool everyone reaches for first and it is still the right answer for a whole class of question. Brendan Gregg's original writeup describes it as producing "a live log of each process for later study", and the upstream example output shows the shape: PID, PPID, RET and ARGS, one row per exec, on a machine-wide stream. That shape is perfect for piping into grep, into a file, or into anything that expects lines. If your question is "did anything anywhere on this box run wget in the last hour", nothing beats it.
What it cannot do is scope to a build. The execsnoop-bpfcc(8) man page lists the whole filter surface: -u USER filters by UID, -n NAME prints "only commands matching this name (regex)", -l LINE matches an argument substring, -x includes failed execs, --cgroupmap and --mntnsmap filter in-kernel by cgroup or mount namespace, and -P PPID is documented as "Trace this parent PID only." Parent, not ancestor. That distinction is the whole problem and it gets its own section below.
bpftrace will attach to tracepoint:sched:sched_process_exec and print whatever fields you ask for, which makes it the right tool when the shape you want does not exist yet: a histogram of exec depth, a count keyed on comm, a per-minute rate. Its -p flag is documented to "attach to the process with or filter actions by PID", and for tracepoints it "will act like a predicate to filter out events not from that pid", which is a single-pid filter rather than a subtree.
Building a subtree scope in bpftrace means writing the fork bookkeeping yourself: a map keyed on pid, an insert on sched_process_fork when the parent is a member, a delete on sched_process_exit. That is perhaps twenty lines and it is genuinely educational, and it is also the exact code that exectop puts in its BPF object so you do not have to maintain it. Reach for bpftrace when the aggregation you want is unusual; reach for a script when the aggregation you want is "what did this build run".
strace -f is scoped correctly by construction. The man page says -f "traces child processes as they are created by currently traced processes as a result of the fork(2), vfork(2) and clone(2) system calls", so the tree is followed without any additional work. It also sees far more than process launches: reads, writes, opens, every syscall the tree makes, which is the reason to use it when your question is about one program's behaviour rather than about which programs ran.
The cost is stated in the same man page: "a traced process runs more slowly than a non-traced one", mitigable with --seccomp-bpf but not eliminated. Attaching ptrace to every process in a parallel build is painful in a specific way that is worse than the raw overhead: the output interleaves across pids, a make -j24 produces syscall traffic in the millions, and finding the four execs you cared about inside it is a text-processing exercise. Filtering with -e trace=execve narrows the syscalls, not the volume of processes.
auditd is the compliance answer and it is the only route here that gives you a log which outlives the terminal. The auditctl(8) man page describes the program as loading "discretionary audit rules" into the kernel, and an execve rule takes the familiar -a exit,always -S execve form. It supports a -F field filter that includes ppid, so a narrow rule is possible, and the records land in the audit log where a SIEM can read them next quarter.
Two things make it the wrong shape for a build. The man page notes that "syscall rules get evaluated for each syscall for every program", so the rule is a property of the machine rather than of your job, and the output is a system-wide stream you filter afterwards rather than a scope you set beforehand. The other is format: an audit record per exec, with the argv split across a0, a1, a2 fields, is designed to be parsed rather than read. If your goal is an answer in the next ninety seconds, that is the wrong end of the trade.
This is the incumbent advice and it deserves fair treatment, because it costs nothing and it is often enough. The Bash manual defines set -x as printing "a trace of simple commands, for commands, case commands, select commands, and arithmetic for commands and their arguments or associated word lists to the standard error after they are expanded and before they are executed". GNU make's --trace is shorthand for --debug=print,why, and -n prints the recipe without running it. On GitHub Actions, ACTIONS_STEP_DEBUG and ACTIONS_RUNNER_DEBUG add step and runner diagnostics to the job log.
Every one of them reports intent, one layer at a time. set -x shows what the shell expanded; it does not show what the Python script that shell invoked went on to exec, unless that script also traces itself. A build is a stack of programs each written in a different language, and shell tracing gives you visibility into exactly the layers that happen to be shell. The kernel does not have that gap: sched_process_exec fires for the Node child of the Python child of the Make recipe with the same fidelity as for the top-level command.
pstree "shows running processes as a tree", rooted at a pid you name or at init, and ps -ejH prints the same relationship in a flat listing. For a live service, a stuck job, or a runaway parallel build that is still running, this is the fastest orientation available and it needs nothing installed.
It is a snapshot, and build processes are the worst possible subject for a snapshot. A cc1 invocation for one source file lives for tens of milliseconds. Run pstree during a compile and you will see whichever handful of processes happened to exist at that instant, with no record of the several hundred that came before and no way to know whether the one you are hunting was among them. Sampling pstree in a loop is the usual next idea, and it trades a guaranteed miss for a probabilistic one.
Package scanners answer a different question and they answer it earlier, which is a genuine advantage. npm audit "submits a description of the dependencies configured in your project to your default registry and asks for a report of known vulnerabilities", checking against advisories rather than against behaviour. Snyk Open Source does software composition analysis over your manifests, scoring findings against exploit maturity and reachability. Socket analyses the package itself, and states plainly that it "examines the behavior of packages, detecting the use of risky APIs or unexpected behaviors" before installation.
They share one boundary, and it is the reason runtime observation is a separate control rather than a redundant one: they tell you about the package, not about this install on this machine. A vendor-neutral write-up of the split puts it as "package scanning checks code and metadata before deployment" while "runtime monitoring watches what the package actually does during install and execution", and its concrete runtime advice is to watch for unexpected child process spawning and outbound connections during install or test runs. Use both. A scanner blocks a merge, which exec tracing cannot do, and exec tracing sees the postinstall that nobody has catalogued yet, which a database cannot.
Watch the build under a tracer scoped to it, and read the counts rather than the events. The finding is almost always one of two shapes: something running per-file that should run once, or a step running twice because two targets both depend on it. Both are invisible in a wall-clock timing breakdown and obvious in a fold list, because a fold list is sorted by how many times each kind of command ran. Under exectop, a real capture reads as 425 execs in 42 seconds at 7.6 per second, with echo at ×124, date at ×90, sleep at ×46, then as at ×27, gcc at ×26 and cc1 at ×26 as the actual compile work.
curl -fsSL https://yeet.cx | sh # install yeet, once
./bin/exectop -- make -j4 # run the build, watch every process it starts
The second column worth reading is fork→exec, the median time between a process being created and its program starting, which in that same capture runs 149µs for echo, 137µs for date and 243µs for gcc. That number separates a slow program from a slow launch. A build where every row shows a normal fork-to-exec median and the wall clock is still bad has a slow program in it; a build where the medians climb as concurrency rises is spending its time on process creation, which is a different fix (fewer, larger invocations) than optimising any single tool.
You cannot, with the flags execsnoop has. The man page filter surface is -u USER, -n NAME, -l LINE, -x, --cgroupmap MAPPATH, --mntnsmap MAPPATH and -P PPID, and none of them expresses "this process and its descendants". The two that come closest are worth knowing precisely. --cgroupmap traces "cgroups in this BPF map only (filtered in-kernel)", which is a real scope if your build already runs in its own cgroup, as it does inside a container. -P PPID traces "this parent PID only", which is one generation.
The general fix is to make membership propagate in the kernel at fork, before the child has a chance to exec anything. Seed a map with one root pid; on sched_process_fork, if the parent is in the map, insert the child; on sched_process_exit, remove it. Then the exec filter is a single hash lookup against that map. This is what exectop does with a traced hash of tgid to depth, and it is the same subtree-scoping move used to follow an AI coding agent's session in an earlier post on auditing what an agent actually ran, applied to a build instead of a session.
Scope also decides what the tool can honestly promise, which is a distinction the flags never surface. Launching the target under the probe means it is held stopped until the probe attaches, so there is no window in which it can fork unobserved. Attaching to something already running cannot make that promise: a process that forked its children before you attached is outside the traced set until it forks again. exectop prints complete tree in the first case and pre-existing children not tracked in the second, rather than letting you assume the first.
Because a build is three or four generations deep and -P matches one. Run make under execsnoop -P <make's pid> and you will see the recipe shells make forked, and nothing they went on to run. The compile chain in the capture above is bash starting gcc, gcc starting cc1 and as, so cc1 and as are grandchildren of the shell and great-grandchildren of make. Twenty-seven as invocations and twenty-six cc1 invocations, the actual work of the build, are all outside a parent-pid filter.
The failure is quiet, which is what makes it expensive. There is no error and no warning. You get output, it looks like a plausible list of commands, and the thing you were hunting is simply not in it, so the natural conclusion is that the build did not run it. Piping the host-wide stream into grep has the opposite failure: you see everything the machine did, including the other seven jobs on the same runner, and you cannot tell which pids belonged to your build without reconstructing the tree from PPID columns after the fact, by which time the intermediate processes are gone.
Capture the exec list on both machines and compare the sets rather than the logs. Differences that produce this failure mode almost always show up as a row that exists in one capture and not the other: a different compiler on the path, a fallback branch taken because a tool was missing, a python3 where the other box used python3.11, a download step that only fires when a cache misses. The build logs will not show it, because both runs printed the same lines from the same script.
yeet run src/probes/capture.js -- $$ 60 > local.txt # on the laptop
yeet run src/probes/capture.js -- $$ 60 > ci.txt # in the job
diff local.txt ci.txt
The headless capture path prints totals, a bucket breakdown, the findings and the top folds as text, then exits, which is what makes it diffable at all. A folded list is a much better diff subject than a raw event stream for the same reason it is a better read: two runs of the same build produce the same rows in a slightly different order with slightly different counts, so diff over an event log is all noise and diff over a fold list is the one row that changed. Note that the fold key ignores positional paths, so a build compiling different files still folds to the same row and the diff stays quiet about the parts that were supposed to differ.
Run the install under a tracer and read what it launched, because the scanner and the tracer answer different halves of the question. npm install executes lifecycle scripts from the package by default; the ignore-scripts config defaults to false, documented as "if true, npm does not run scripts specified in package.json files", which is the blunt instrument and breaks any package with a native build step. The observation route keeps the install working and records what it did.
./bin/exectop -- npm install # or: npm ci, or your package manager of choice
What you are looking for is a process the install had no reason to start. A node-gyp chain, python3, make and cc1plus are what a native module looks like and they will dominate the fold list by count. A single curl fetching from a host nobody mentioned, a chmod widening permissions on something in /tmp, or a read against a credential path is the other shape, and it will be near the bottom of a chronological log and at the top of a ranked one. If you want the version of this demonstration where a deliberately obfuscated step gets caught reaching for credentials, the GitHub Actions audit post already walks through one with execsnoop and opensnoop, and that beat does not need repeating here.
Be clear about what this is not. It is not a gate. There are no rules to configure, no database of known-bad releases, and nothing is blocked, delayed or modified. A package that does its damage inside Node without launching a program is invisible: fetching a URL with fetch() looks like nothing at the exec layer, while fetching it with curl is a row. Treat the output as a look at what happened on this machine just now, and keep the scanner for the part where a merge gets stopped.
Rarity alone does not work, and the measurement that proves it is worth carrying: against a real npm install, 11 of 34 distinct commands ran exactly once. A rule that flags anything running once would light up a third of an ordinary install, and a panel that is wrong a third of the time gets ignored inside a week. Anyone building this heuristic themselves lands on rarity first, and then has to add the second half.
The second half is behaviour observed in the arguments. exectop shows a row in its doesn't fit panel only if it ran three times or fewer and matched one of seven observed behaviours:
curl or wget in a step whose job is compilation, where the URL is right there in the argv.curl … | sh shape, which is a legitimate install idiom and a bad thing to find inside a dependency's postinstall.eval whose argument was assembled rather than written.chmod that adds bits, especially on something just written.~/.ssh, a cloud credentials file, or a token store./tmp. An archive expanded somewhere outside the build tree.Every reason names something present in the arguments rather than a guess about intent, and the threshold does the rest of the work: a build that legitimately fetches six times stays silent, because six is that build's normal. What the rule cannot do sits in the same place as its design. These are pattern matches on observed command lines, so anything that renames a binary, assembles its argument string at runtime, or avoids launching a process at all will not be flagged. It surfaces what happened, and it is not a control that an adversary can be prevented from evading.
Yes, but check which half of the tool you are running, because the TUI and the data layer have different requirements. exectop's terminal interface needs a real TTY and refuses to start without one, which is correct behaviour for a program that reflows on resize, and useless in a job whose stdout is a log file. The data layer runs standalone and prints a plain-text report:
yeet run src/probes/capture.js -- <root-pid> 30
It seeds the traced set from the root pid you give it, aggregates for the number of seconds you name, prints the totals, the bucket breakdown, the findings and the top folds, then exits. There is no --json mode today, so anything machine-read is parsing text or adding a sink in the ring-buffer callback. The other routes have their own CI shapes: execsnoop redirects to a file happily, auditd is already writing to one, and strace -f -o trace.log works but leaves you with a file measured in hundreds of megabytes for a real build.
One thing that is not required, and is worth stating because it is the usual blocker on a managed runner: there is no agent to deploy and nothing gets added to the process you are watching. Installation is the yeet daemon on the host, and yeet run itself is unprivileged and never takes sudo. On a runner where you cannot install a daemon at all, strace -f on the job's own pid is the route that needs the least from the machine, and you pay for it in output volume and in the slowdown the man page warns about.
It depends entirely on where the filtering happens, and the three routes are not close. A ptrace-based tracer stops and resumes the target around syscalls, which is why the strace man page says a traced process runs more slowly. An audit rule is evaluated in the kernel for every matching syscall from every program on the box, whether or not you care about that program. A tracepoint-based probe with an in-kernel filter does one hash lookup on an exec by a process outside your scope and returns, so the cost tracks the application you are watching rather than total activity on the host. Kernel documentation describes a disabled tracepoint as costing "a tiny time penalty (checking a condition for a branch)".
Exec is also a rare event by the standards of kernel tracing. A busy build is hundreds of execs per second, where a network path handling the same instrumentation technique sees tens of thousands of packets. The ceiling that does exist is a delivery ceiling rather than a CPU one: exectop measures a ring-buffer limit around 2,700 execs per second, because each record carries a 1 KiB argument window and the ring is bound by bytes moved. Under a deliberate fork storm of 96,000 execs across 24 workers, 36,000 were captured and 60,000 dropped. The drop count is reported rather than hidden, the verdict line reads 59,431 dropped, and the headless report marks its numbers as a floor. A real npm install runs at tens per second and never approaches it; a make -j24 on a large machine can.
Attach to the container's process tree and read the answer, including the empty one. Container mode resolves the container name to its root pid and scopes by cgroup where one resolves, so the question becomes "what is this container launching" rather than "what is this host doing".
yeet run . -- --container api
There are two useful outcomes and one of them is silence. A list tells you the container is shelling out and to what, which usually names the culprit directly: a health check spawning a process per interval, a library shelling out to git, a sidecar-shaped behaviour hiding inside the main container. Silence tells you nothing in that container is starting processes, which means the problem is inside the application, and that is a real result rather than a failed measurement. A long-running service that has finished starting up execs almost nothing, so an empty screen is the expected state for a healthy web server and a meaningful one for a suspect container.
The caveat belongs right here, at the point of decision, because it decides whether the silence means anything. Attaching to a running container only sees what it starts from that point on, since membership propagates at fork. A supervisor that forked all its workers at startup shows nothing until it forks again, and the status bar says pre-existing children not tracked rather than implying the tree is complete. If the container is restartable, restarting it under launch mode removes the ambiguity; if it is not, treat silence as weaker evidence than a list.
The boundary is the same for every tool on this page, because it is a property of the hook rather than of the implementation. sched_process_exec fires when a program is installed into a process, so anything that never becomes a program is outside the frame. In practice that means five things, and knowing them is what keeps a clean screen from being read as a clean bill of health.
agent-lock is the yeet script that hooks lsm/file_open and returns -EPERM for a path outside a jail, which is enforcement rather than observation. For HTTP see container-traffic, and for raw packets pktscope.exectop copies a fixed 1024-byte window of the argument blob and marks the record truncated, so a very long compiler invocation is cut off while the program, its flags and the timing stay correct.comm is 16 bytes in the kernel, so long names arrive truncated, and that truncation is the kernel's rather than the tool's.auditd is the route that answers the durability question, at the cost of everything else on this page.Where a limit has a sibling that covers it, take the sibling rather than stretching the tool. CPU profiling of one process is hotspot, which deliberately excludes forked children, exactly inverting what a process-launch monitor covers. That inversion is a good way to check which question you are actually asking: if you care about what one program spends its cycles on, you want a profiler, and if you care about which programs existed, you want the exec stream.
Every route here captures execs correctly. They differ on the two things that decide whether you get an answer: whether you can scope to one build and its grandchildren, and whether anything reduces the volume once you have. strace -f scopes perfectly and reduces nothing, at ptrace cost. bcc's execsnoop reduces nothing and scopes to one parent generation, which is the wrong shape for a build tree, and it remains the best flat, greppable, host-wide stream there is. auditd is the answer when the record has to survive the reboot and be parsed by something else next quarter. set -x, make --trace and Actions debug logging report intent, which is a genuinely different fact from execution and often the one you actually want.
exectop is the answer to one question: what did this application launch, when the answer is hundreds of events and you need it grouped. It scopes by launch, container or pid, propagates membership at fork so grandchildren are included, folds repeats into one row per kind, and ranks the rare-plus-suspicious above the rest. It blocks nothing, keeps nothing after you quit, and cannot see a package that never execs. Where you need a boundary rather than a record, agent-lock enforces on file opens through BPF-LSM, and where you need a merge stopped, Socket and Snyk do the pre-execution half that no runtime tool can.
It depends on the route. auditd rules and bcc tools require root. strace needs ptrace permission on the target, which usually means the same user or root, and can be blocked outright by a hardened ptrace_scope setting. With yeet, the daemon handles the privileged load and yeet run is unprivileged, so a sudo yeet run in an example is a mistake rather than a requirement.
Tracepoint-based tracing runs on the host kernel, which containers share, so execs inside a container are visible from the host with no changes to the container image. That is what makes it usable on distroless images where there is no shell to exec into. Resolving a container name to its process tree is the part that needs help: exectop's container mode needs Docker reachable from the host running the probe, because it looks the root pid up through the system graph.
For CO-RE-based eBPF tooling the practical floor is a kernel built with BTF, meaning CONFIG_DEBUG_INFO_BTF=y, which is the default on current Arch, Fedora, Ubuntu and Debian. exectop is verified on 6.1, 6.6, 6.12 and bpf-next, and CO-RE means no per-kernel recompile. The sched_process_exec, sched_process_fork and sched_process_exit tracepoints are long-standing and present across that range.
Yes. The record comes from the kernel at exec time, so a binary copied to another name, a static build, or a script invoked through an interpreter all produce an exec with the arguments the kernel was handed. What renaming defeats is not the capture but the heuristics: a ranking rule that matches on the program name stops matching when the program is called something else, which is why an empty findings panel is not evidence of nothing happening.
exectop's record carries pid, ppid, comm, argv, depth and the fork-to-exec time, not the environment. That cuts both ways. A secret passed as an environment variable does not appear in the capture, and a secret passed on a command line does, in full, in the argv column, which is worth knowing before pasting a capture into a ticket.
It is invisible to an exec tracer, and this is a real gap rather than a corner case. A worker pool that forks copies of itself and never replaces their memory with a new program produces exactly one exec, at the top. The bcc example output notes the same limitation. If the fork itself is what you need to see, the sched_process_fork tracepoint is the hook, and pstree on the live process is the zero-install version of the same answer.
Not from a terminal tool, which keeps nothing after you quit. In CI the practical answer is to redirect the headless report to a file and upload it as a build artifact, which gives you a per-job record without a retention system. If the requirement is fleet-wide retention with a query language across machines, that is auditd feeding a log pipeline, or a commercial platform, and neither of those is what a single-host terminal tool is for.
-u USER filter by UID or username, -n NAME "only print command lines matching this name (regex)", -l LINE matching an argument substring, -x "include failed exec()s", --cgroupmap MAPPATH and --mntnsmap MAPPATH "filtered in-kernel", and -P PPID documented as "Trace this parent PID only", with no option expressing a process subtree.-P PPID as "trace this parent PID only" and -n NAME as regex matching on any arg; records the structural limitation that the tool follows the fork-then-exec sequence, so worker processes that fork without ever calling exec never appear.-f "traces child processes as they are created by currently traced processes as a result of the fork(2), vfork(2) and clone(2) system calls", which is correct subtree scoping by construction; states that "a traced process runs more slowly than a non-traced one" and that the impact "can be mitigated by using the --seccomp-bpf option"; describes the tool as intercepting and recording syscalls and signals.-a exit,always -S execve form, -F supports field filters including ppid, and the page states that "syscall rules get evaluated for each syscall for every program", which is why an audit rule is a property of the host rather than of one job.set -x as printing "a trace of simple commands, for commands, case commands, select commands, and arithmetic for commands and their arguments or associated word lists to the standard error after they are expanded and before they are executed", preceded by the expanded value of PS4; the trace is emitted by the shell about its own expansions, not about what a child program later executes.ACTIONS_RUNNER_DEBUG enables runner diagnostic logging covering "the runner process log, which includes information about coordinating and setting up runners to execute jobs" and the worker process log; ACTIONS_STEP_DEBUG shows "more debug events" in step logs; both are set as repository secrets or variables, and both report the runner's own view rather than process execution./-/npm/v1/security/advisories/bulk, packages with no version field are excluded, and the response carries advisory objects with severity, vulnerable ranges and IDs, from which npm computes meta-vulnerabilities.mm->arg_start rather than by walking userspace pointers, and why a stable tracepoint beats hooking sys_enter_execve.lsm/file_open that confines a process tree to one directory and returns -EPERM for anything outside it.See your machine the way the kernel sees it.
Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.