Skip to main content

How CI/lock captures files

The material and product attestors don't decide how the file set is gathered — they commit a Merkle root over whatever set the capture pipeline hands them. --capture-mode picks where that set comes from:

--capture-modeSourcePlatform
walkDirectory snapshot before (materials) and after (products) the commandAll (Linux, macOS, Windows)
traceSyscall tracing — what the process actually opened and wrote (requires --trace)Linux
auto (default)Trace events when --trace is on, otherwise the walk
imaKernel IMA measurements (requires CONFIG_IMA) — not yet wired, reserved for a future releaseLinux

On top of whichever mode is active, fanotify can supply kernel-synchronous content hashes; it's controlled by --hardening.

Directory walk (the portable default)

Without --trace, CI/lock records artifacts by walking --workingdir:

  • Materials — every regular file present before the command runs is hashed (sha256), bounded to the working directory, honouring --dirhash-glob. The input baseline.
  • Products — after the command exits, files that were created or changed during the command window become products. CI/lock uses mtime to catch byte-identical rebuilds a pure content diff would miss.

The walk is simple, deterministic, and works everywhere, but it sees only the net before/after state of the working directory — not what the process actually touched, and nothing outside --workingdir.

Syscall tracing (what the process actually did)

On Linux, --trace observes the process tree's syscalls so CI/lock knows which files were opened (inputs) and written (outputs) — including files outside the working directory — instead of inferring it from a snapshot. There are two backends, selected with the optional :<backend> suffix on the trace capture mode:

  • ptrace+seccomp — the portable, always-available backend. A seccomp filter traps the file-relevant syscalls and CI/lock inspects them via ptrace. This is what most runs use in practice.
  • eBPF — a CO-RE eBPF program that observes the same events with less per-syscall overhead, used when the kernel supports it.

--capture-mode trace:auto (the default when --trace is on) probes eBPF and, if it isn't available, silently falls back to ptrace+seccomp — so tracing works either way. --capture-mode trace:ebpf forces eBPF and fails loudly if it can't load; --capture-mode trace:ptrace skips the probe and uses ptrace directly.

eBPF availability varies a lot by host. On many CI runners the eBPF object can't load (kernel/BTF skew or missing capabilities), so auto lands on ptrace+seccomp. Use --diagnose to see which backend was chosen and why.

fanotify (kernel-synchronous content)

Tracing reports that a file was opened or written; fanotify supplies the actual content hash at the moment it happens, which is race-tight against a file that's modified again later in the build:

  • Materials are hashed at FAN_OPEN_PERM time (each inode hashed once via FAN_MARK_IGNORE).
  • Products are hashed at FAN_CLOSE_WRITE, then anchored to the set that still exists at process exit.

fanotify is enabled by --hardening standard (the default) and required by --hardening strict; toggle it directly with CILOCK_FANOTIFY.

Choosing a mode

  • Local / portable / non-Linux: the default walk is fine — cilock run -- <cmd>.
  • CI on Linux, accurate input/output attribution: add --trace. --capture-mode auto uses tracing when available (eBPF if the kernel allows, else ptrace+seccomp) and falls back to the walk otherwise.
  • Release-grade, fail-closed: --hardening strict --trace requires fanotify + fs-verity and fails on any dropped event (--require-zero-drops).

See the material and product attestor pages for the predicate each emits, and the CLI reference for every capture flag.