Skip to main content
Every recorded job produces one Execution Profile. An execution chain is one path through the process tree, from the runner’s root to an action — today, an outbound connection. The profile ties every chain to the commit, workflow, and job that produced it, and posts back to your PR.

What’s recorded

One record, four surfaces. Jibril, an eBPF sensor, runs alongside your workflow and records network and process activity at the kernel — with full lineage and timing.

Reading the comment

The comment records each action and the execution chain behind it.
Execution Profiles recorded for 5 jobs, triggered by 6e5d0d4
25 execution chains · 12 destinations · recorded at the kernel by Garnet · 2026-07-13 23:54:31 UTC
▸ ci / docs-build ↗ · Run workload reached 1 destination
▸ ci / install-only ↗ · Run workload reached 1 destination
▾ ci / lint ↗ · 3 chains · 3 destinations
systemd
└─ hosted-compute-agent
   ├─ sudo
   │  └─ provjobd
   │     ├─ → localhost (dns resolver)
   │     └─ → hosted-compute-watchdog-prod-iad-02[.]githubapp
   └─ → 140.82.113.24

View this job’s Execution Profile in Garnet →

▸ ci / typecheck ↗ · 3 chains · 3 destinations
▾ ci / workload-egress ↗ · Run workload reached 5 destinations
Runner.Worker
└─ bash
   └─ node
      ├─ dash
      │  └─ node
      │     ├─ dash
      │     │  └─ curl
      │     │     └─ → httpbin[.]org
      │     ├─ → registry.npmjs[.]org
      │     ├─ → api.garnet[.]ai
      │     └─ → github[.]com
      └─ → registry.npmjs[.]org
▸ dns + runner substrate · 6 chains
systemd
└─ hosted-compute-agent
   ├─ sudo
   │  └─ provjobd
   │     ├─ → localhost (dns resolver)
   │     └─ → hosted-compute-watchdog-prod-iad-02[.]githubapp
   └─ → 140.82.113.23
Runner.Worker
└─ bash
   └─ node
      ├─ dash
      │  └─ node
      │     ├─ dash
      │     │  └─ curl
      │     │     └─ → localhost (dns resolver)
      │     └─ → localhost (dns resolver)
      └─ → localhost (dns resolver)

View this job’s Execution Profile in Garnet →

A recorded comment, two folds open. workload-egress ran a workflow step that reached five destinations; lint recorded no step-attributed chain, so its row carries counts instead of a sentence.

Top to bottom:
  • Headline — what was recorded, and the commit that triggered it.
  • Metadata line — the run’s counts, kernel provenance, and recording time, one fact per · segment.
  • One fold per job — headed workflow / job ↗, linking to its Actions run. The rest of the row names which recorded steps reached how many destinations.
  • The tree — only processes with recorded egress. Destinations are → name leaves under the process that opened them, defanged at the final dot. Resolver chatter and unattributed runner chains sit in the nested dns + runner substrate fold.
  • Per-job permalink — opens the job’s public run report.
  • A job with no egress renders one quiet line: no outbound destinations recorded. Before the first profile lands, the comment shows a waiting state (⏳) and fills in as jobs finish.
  • The 💡 How to read this explainer sits at the bottom, open on the first recorded result and collapsed on later updates.
  • Coverage is only N job(s) recorded on this commit — no expected-job denominator.
  • Destinations are never compressed into ×N. A bare IP appears only when no remote name was recorded; secondary recorded names never create extra rows.
  • Large comments truncate deterministically and say so: rendered X of Y destination associations — never silently.
  • The comment omits ports, protocol, and [pid · process] suffixes. Those stay in the Garnet Execution Summary and the dashboard.
  • Display only: hostnames are defanged (registry.npmjs[.]org), step names drop the runner’s ordinal prefix and any unexpanded ${{ matrix.name }} expression, and process names drop a trailing run of four or more digits (provjobd1326539233provjobd). The record keeps every raw value.

What changed since the last recorded commit

Once a pull request has two recorded commits, the comment stops being a snapshot and starts being a comparison against the previous profiled commit.
A Garnet comparison comment: the metadata line reads changed since 551aa3f, the job row leads with +1 −0, and the tree renders as a diff with one added destination, httpbin[.]org

A recorded comparison on the demo repository. The diff added one line — a postinstall hook. The run reached a host the diff never names.

Three things change in the comment:
  • The metadata line carries changed since <sha7>, linked to the commit it compared against — or no change since, when nothing moved.
  • The job row leads with the delta+1 −0 — so the left edge is the scan column, and a changed job opens by default.
  • The tree becomes a diff, headed @@ <head> vs <previous> @@. + is a destination or chain recorded now and not before, is one recorded before and not now, unmarked lines are in both. A destination shows as both when its execution chain changed.
Only recorded commits count. A push whose jobs never recorded is not a comparison point, and the first recorded commit on a pull request has nothing to compare against, so it renders as a snapshot.

Attribution

The tree separates your workload from the machinery around it:
Only two exist, and the record has to prove them: (dns resolver) — a loopback address on port 53 — and (instance metadata)169.254.169.254, 169.254.170.2, fd00:ec2::254. Everything else renders exactly as recorded, untagged.
Typography is the machine contract for attribution: workload lineage is bold (<strong>), scaffolding is italic (<em>) — see Consume the Runtime Review. Attribution context, not trust.

One comment per PR

One sticky comment, updated in place on every push and as each job’s profile lands, pinned to the head commit it records. The Action never fails your job — it records the run.

What Garnet sees

The only secret Garnet uses is the API token you pass it. It never reads your other secrets, and never writes to your repo.
  • Metadata, not contents — an Execution Profile carries execution chains and outbound domains, IPs, ports, and protocols, not your source or your secrets.
  • Observe-only — Jibril reads syscall-level events. Every eBPF program is checked by the kernel verifier before it loads and runs sandboxed.
  • No pipeline changes — no sidecars, no proxies, no reordering of your steps.
Attaching eBPF programs requires sudo during install. ubuntu-latest runners include it by default. Sudo loads the sensor — it doesn’t touch your build.
Store your GARNET_API_TOKEN in CI secrets or a vault — never commit or log it in plain text.
Garnet is SOC 2 Type II. Controls, subprocessors, and security documentation live in the Trust Center.

The public run report

Every job’s permalink opens a permanent report page:
A public Garnet run report showing the process tree of a CI job with per-destination IPs, ports, protocols, and step attribution

The report behind the comparison above — the same record with addresses, ports, protocols, and the step each destination belongs to.

Share it in a PR thread or an incident channel to point teammates at the exact run — or use Share on the report page. The URL fails closed by design:
  • ?profile=<profile-id> is required and exact. Wrong or missing selector → 404, never a silent fallback. The comment and the Garnet Execution Summary carry it for you.
  • There is no run index. Bare /public/runs/<run-id> — or a ?job= query — is the same 404.
  • Private, revoked, unconsented, or no-longer-public records return the same 404. Publication needs explicit consent plus backend-confirmed public repo visibility, rechecked on every visit.
The same record is machine-readable JSON at app.garnet.ai/api/public/runs/<run-id>?profile=<profile-id> — same selector rules. See Consume the Runtime Review.

README badge

A published Execution Profile can back a Garnet Runtime Profiled badge — it says the run was recorded, links to the report, and carries no verdict:
Both ids are in the report’s URL. A badge link without ?profile= is the same 404 as any other bare run URL. Next: Consume the Runtime Review