Linux Telemetry Schema
This page documents the complete schema for all telemetry event types collected by Workshop from Santa agents on Linux.
Each event type below is also the table name in SQL queries, prefixed with linux_: exec fields live in linux_exec_2026, linux_exec_202603, or linux_exec_20260315 (see table naming).
The prefix exists because fork and exit are collected on macOS too, with entirely different columns.
Columns drift across Santa versions, so DESCRIBE linux_exec_20260315 on your own data is the authoritative list.
Linux telemetry is not shared with the macOS schema, including the base fields.
A telemetry field with the same name on both platforms does not necessarily mean the same thing, and a query written against one platform will not run against the other.
Package inventory is the exception: its platform-independent packages table has the same columns on every host.
Contents
Event tables
- Process Events: linux_fork, linux_exec, linux_exit
- Inventory Events: packages
Common types
Credentials, Namespaces, Cgroup, Process, Lineage, Filesystem, FileIdentity, Digest, File, FileDescriptor, Tty, Image, WorkloadOwner, KubernetesPod, Container
Base Fields
Every Linux telemetry table carries these identity and timing columns.
| Field | Type | Description |
|---|---|---|
| EventID | text | Unique identifier for the event |
| MachineID | text | The unique machine ID (host UUID) |
| Hostname | text | The hostname of the machine at the time of the event |
| BootID | text | The kernel’s boot identifier, stable for the lifetime of one boot |
| EventTime | timestamp | When the event occurred |
| ProcessedTime | timestamp | When Workshop processed the event |
| OperatingSystem | text | The platform the telemetry came from, always linux in these tables |
Process Events
linux_fork
A process creating another process.
SharedThreadGroup is what separates a new thread of the same program from a genuinely independent process, and it decides that on its own.
SharedMm and SharedFiles each report one resource shared independently of that: a thread commonly shares both, but neither is required for thread-group membership, and either can be shared by a task that is a separate process.
| Field | Type | Description |
|---|---|---|
| Subject | Lineage | The process performing the creation, with the process that started it |
| Object | Process | The newly created process |
| SharedThreadGroup | boolean | Whether the new task joined its creator’s thread group, making it a thread of the same program |
| SharedMm | boolean | Whether the new task shares its creator’s address space |
| SharedFiles | boolean | Whether the new task shares its creator’s file descriptor table |
| Container | Container | The container the subject ran in. Absent when it ran directly on the host |
linux_exec
A process replacing itself with a new program.
| Field | Type | Description |
|---|---|---|
| Subject | Lineage | The process performing the execution and the process that started it, as they are after the change took effect |
| Comm | text | The short name the new program will be known by, at most 16 characters. Chosen by the program, so not evidence of what ran |
| Executable | File | The canonical path of the executable image that actually ran |
| Invoked | File | What was asked for. For a script this is the script, where Executable is the interpreter |
| Interpreted | boolean | Whether Invoked and Executable differ because an interpreter handled the request |
| WorkingDirectory | File | The working directory, needed to make sense of any relative path in Args. Only Path and Type are populated |
| Root | File | The process’s root directory, which is how a process confined to a subtree becomes visible. Only Path and Type are populated |
| FileDescriptors | FileDescriptor array | The standard descriptors the program inherited |
| Tty | Tty | The terminal the process is attached to |
| Args | text array | Command-line arguments |
| Envs | text array | Environment variables |
| Argc | number | An upper bound on how many entries Args holds, not a guarantee |
| ArgvEnvpCopied | number | Bytes of arguments and environment captured. Zero when capture failed outright |
| ArgvEnvpTruncated | boolean | When set, the absence of an expected argument means nothing |
| Container | Container | The container the subject ran in. Absent when it ran directly on the host |
linux_exit
A process terminating.
Signal is meaningful only when ExitFromSignal is set, and ExitStatus only when it is not.
ExitCode is retained so nothing is lost, but it cannot be interpreted without ExitFromSignal; read the decoded fields instead.
| Field | Type | Description |
|---|---|---|
| Subject | Lineage | The terminating process, with the process that started it |
| ExitCode | number | The raw packed status value a waiting parent receives. Cannot be interpreted without ExitFromSignal; read Signal and ExitStatus instead |
| ExitFromSignal | boolean | Whether the process was killed by a signal rather than ending on its own |
| Signal | number | The signal that killed the process, meaningful only when ExitFromSignal is set |
| ExitStatus | number | The status the process chose to end with, meaningful only when ExitFromSignal is not set |
| CoreDumped | boolean | Whether a memory dump was started, not that one was completed or written anywhere |
| GroupDead | boolean | Whether a whole program ended, as opposed to one thread of a still-running program |
| Container | Container | The container the subject ran in. Absent when it ran directly on the host |
Inventory Events
Unlike every other table on this page, inventory tables are not populated by the continuous telemetry stream. They are produced on demand by the Package Inventory command (Hosts → Commands → Run command), which asks each targeted host to run a read-only scan and upload the results into its normal telemetry prefix. A host that has never been scanned has no rows.
Two consequences worth knowing when querying:
BootSessionUUIDis always empty. An on-demand scan isn’t tied to a boot session.EventTimeis the scan time, not the time a package was installed — the scan observes current state and has no visibility into when it came to be.
packages
One row per package discovered on a host. Every ecosystem shares this single table,
distinguished by Ecosystem, so a fleet-wide query needs no unions.
| Field | Type | Description |
|---|---|---|
| EventID | text | Unique identifier for the inventory record |
| MachineID | text | The unique machine ID (host UUID) |
| Hostname | text | The hostname of the scanned host |
| BootSessionUUID | text | Always empty because an inventory scan is not tied to a boot session |
| EventTime | timestamp | When the inventory scan observed the package |
| ProcessedTime | timestamp | When Workshop processed the inventory record |
| OperatingSystem | text | The platform the scanned host is running |
| RecordType | text | Always package in this table |
| RunID | text | Identifier shared by every row from one scan; use it to isolate a single scan’s results |
| Profile | text | Scan profile that produced the row: baseline, project, or deep |
| Ecosystem | text | npm, pypi, go, rubygems, packagist, mcp, editor-extension, browser-extension, homebrew, agent-skill, or nix |
| PackageName | text | Package name as written in the manifest or lock file |
| NormalizedName | text | Ecosystem-normalized name — join on this rather than PackageName |
| Version | text | Installed version. Empty when no exact version could be determined |
| ProjectPath | text | Root of the project the package belongs to, for project-scoped finds |
| RootKind | text | Why the containing directory was walked: global_package_root, user_package_root, project_root, editor_extension_root, browser_extension_root, mcp_config_root, homebrew_root, agent_skill_root, deep_home_root, or unknown |
| InstallScope | text | Ecosystem-specific dependency scope (e.g. prod/dev for npm and pnpm, indirect for Go modules) |
| PackageManager | text | Manager that installed the package (e.g. npm, pnpm, homebrew, firefox-extension) |
| SourceType | text | Kind of evidence the row came from (e.g. package.json, browser-extension) |
| SourceFile | text | Path to the manifest, lock file, or metadata file the row was read from |
| DirectDependency | boolean | Whether the package is directly depended on rather than transitive. Null when the ecosystem can’t distinguish |
| HasLifecycleScripts | boolean | Whether the package declares install-time lifecycle scripts — these execute on install, so they are a supply-chain execution surface |
| LifecycleScripts | text array | Names of the declared lifecycle scripts |
| Confidence | text | How certain the identification is: high, or medium when the name or version had to be inferred |
| RequestedSpec | text | For MCP entries configured by spec, the requested selector (e.g. @playwright/mcp@latest) with PackageName normalized to the bare name |
| LocalAlias | text | Local name assigned in a config file, where that differs from the package it resolves to. Set only for mcp (the key under mcpServers) and agent-skill (the local skill name) |
Common Nested Types
The following types are used throughout the Linux telemetry schema to represent shared data structures.
Credentials
The identity a process runs as. Two sets are carried on every process: the real set records who started it, the effective set is what permission checks are made against, so a program that has changed identity is still attributable to whoever launched it.
| Field | Type | Description |
|---|---|---|
| UID | number | The real owner of the process, and who is able to signal it |
| GID | number | The real group of the process |
| SUID | number | Saved user ID, which a process that has dropped privilege can return to |
| SGID | number | Saved group ID, the group equivalent of SUID |
| EUID | number | The process’s privileges for most non-filesystem access |
| EGID | number | The group equivalent of EUID |
| FSUID | number | The identity used when accessing filesystem objects |
| FSGID | number | The group equivalent of FSUID |
| CapInheritable | number | Inheritable capability set, as a bitmask |
| CapPermitted | number | Permitted capability set, as a bitmask |
| CapEffective | number | Effective capability set, as a bitmask |
| CapBounding | number | Bounding capability set, as a bitmask |
| CapAmbient | number | Ambient capability set, as a bitmask |
| Securebits | number | The securebits flags in force for the process |
Namespaces
The isolated views of system resources a process sits in. Each value identifies one view: two processes sharing a value share that resource, and differing values are the clearest signal that a process is isolated from the host, whether by a container runtime, a sandbox, or a program that has isolated itself. These are operating system namespaces and have nothing to do with the Kubernetes namespace on KubernetesPod.
| Field | Type | Description |
|---|---|---|
| UTS | number | Hostname and domain name |
| IPC | number | Shared memory, semaphores and message queues |
| Mnt | number | Which filesystems are mounted, and where |
| PID | number | Which other processes are visible, and under what numbers |
| PIDForChildren | number | The view processes started from here will be placed in. Differs from PID only between requesting a new view and starting the first child |
| Net | number | Network interfaces, addresses, routes and ports |
| Time | number | The system clock offsets a process observes |
| TimeForChildren | number | Same requester-versus-children distinction as PIDForChildren |
| Cgroup | number | How much of the resource-control hierarchy is visible from here |
| User | number | How user and group identities map to those on the host, which is how an unprivileged user can appear as an administrator inside a container |
Cgroup
The resource-control group a process belongs to, as opposed to Namespaces.Cgroup, which describes only how much of the hierarchy the process can see. A process is always in one, container or not.
| Field | Type | Description |
|---|---|---|
| ID | number | Unique for as long as the group exists and not reused while it does, which makes it the key for attributing a process to a container |
| Level | number | How far below the root of the hierarchy the group sits, where zero is the root itself. A container’s group is never at the root |
Process
A running program, or one thread of it.
PID alone does not identify a process over time because process numbers are recycled; PID together with StartBoottime does, within one boot of the host.
Build identity on StartBoottime rather than StartTime, which is derived from the wall clock and inherits its error.
| Field | Type | Description |
|---|---|---|
| PID | number | Process ID as seen from the host |
| NSPID | number | Process ID as seen inside the process’s own PID namespace |
| TGID | number | Thread group ID, the ID of the program this thread belongs to |
| NSTGID | number | Thread group ID as seen inside the process’s own PID namespace |
| StartBoottime | number | Nanoseconds since boot at which the process started |
| StartTime | timestamp | Wall-clock time the process started |
| PGID | number | Process group ID |
| SID | number | Session ID |
| LoginUID | number | The user who logged in, surviving later changes of identity. 4294967295 when unset |
| SessionID | number | An identifier for that login. 4294967295 when unset |
| RealCredentials | Credentials | The identity the process was started with |
| Credentials | Credentials | The identity the process runs as now |
| Namespaces | Namespaces | The kernel namespaces the process sits in |
| Cgroup | Cgroup | The specific resource-control group the process belongs to, as opposed to Namespaces.Cgroup, which describes only how much of the hierarchy it can see. The primary key for attributing a process to a container |
| Executable | File | The running program. Only Identity is populated here |
| NoNewPrivs | boolean | Whether the process is barred from gaining privileges through exec |
Lineage
A process together with the process that started it, captured at the same moment so the pair is internally consistent.
| Field | Type | Description |
|---|---|---|
| Task | Process | The process itself |
| Parent | Process | The process that started it |
Filesystem
The filesystem a file resides on. Where a file lives is often as telling as what it contains: an executable on an in-memory or layered filesystem did not come from the host’s own installation.
| Field | Type | Description |
|---|---|---|
| Type | text | Coarse filesystem type, e.g. FILESYSTEM_TYPE_OVERLAYFS, FILESYSTEM_TYPE_TMPFS |
| Magic | number | The exact filesystem magic value the kernel reported, set even when Type is unknown |
FileIdentity
What distinguishes one file from another independently of the name it is reached by.
Two observations agreeing on Dev and Ino describe the same file even under different paths; agreeing on Size and Mtime as well means it has not changed in between.
| Field | Type | Description |
|---|---|---|
| Dev | number | Device ID |
| Ino | number | Inode number |
| Size | number | File size in bytes |
| Atime | timestamp | Last access time |
| Mtime | timestamp | Last modification time |
| Ctime | timestamp | Last status change time |
Digest
A measurement of a file’s contents, which is what allows the same binary to be recognised wherever it appears and however it is named.
| Field | Type | Description |
|---|---|---|
| Algorithm | text | Digest algorithm, e.g. HASH_ALGORITHM_SHA256 |
| Value | text | Hex-encoded digest value |
| Valid | boolean | False when no measurement was available, in which case the other fields are meaningless |
File
Everything known about a single file. Any observation populates the subset it actually has, so an absent field means “not observed”, never “observed to be empty”.
| Field | Type | Description |
|---|---|---|
| Path | text | File path |
| Identity | FileIdentity | Device, inode, size and timestamps |
| Type | text | The kind of file, e.g. FILE_TYPE_REGULAR |
| Filesystem | Filesystem | The filesystem the file resides on |
| Digest | Digest | Measurement of the file’s contents |
| Deleted | boolean | Whether the file had already been removed when it was observed. Null when the file itself was not inspected |
| PathTruncated | boolean | Whether Path ran out of room before it was complete. Null exactly where Path was not observed |
FileDescriptor
A numbered channel a program inherited when it started. Only 0, 1 and 2 are reported. A pipe or socket where a terminal is expected is a strong indication that a program’s output is being captured.
| Field | Type | Description |
|---|---|---|
| FD | number | File descriptor number |
| File | File | What the descriptor refers to |
Tty
The terminal a process is attached to.
| Field | Type | Description |
|---|---|---|
| Major | number | Major device number |
| Minor | number | Minor device number |
| Name | text | Terminal device name |
| Present | boolean | False for a service, which has no terminal; the other fields then carry nothing |
Image
The image a container was created from.
| Field | Type | Description |
|---|---|---|
| Reference | text | The image reference, which is mutable and reusable |
| Digest | text | The image digest, which is stable and always means one image |
WorkloadOwner
What ultimately created and owns a pod. A pod’s own name is generated and replaced every time it is rescheduled, so this is the durable identity to attribute activity to.
| Field | Type | Description |
|---|---|---|
| Kind | text | Controller kind, e.g. Deployment. Clusters routinely add their own kinds |
| Name | text | Controller name |
KubernetesPod
Where a container sits within an orchestrator, populated only when the host is managed by one.
Namespace here is a Kubernetes namespace, a tenancy boundary within a cluster, which is a wholly different concept to the kernel Namespaces on a process.
Nothing relates the two.
| Field | Type | Description |
|---|---|---|
| Name | text | Pod name, regenerated whenever the pod is rescheduled |
| Namespace | text | Kubernetes namespace the pod runs in |
| UID | text | Pod UID |
| ContainerName | text | Name of the container within the pod |
| Owner | WorkloadOwner | The controller that owns the pod |
| ServiceAccount | text | The identity the pod’s processes present to the cluster |
Container
The container an event occurred inside.
CgroupPath is observed directly and is present whenever the activity happened inside any resource-control group.
Everything below it is resolved against the local container runtime and is absent when the runtime is unknown or unreachable, or when the activity did not happen inside a container at all.
| Field | Type | Description |
|---|---|---|
| CgroupPath | text | The cgroup path the activity happened in |
| Runtime | text | The container runtime, e.g. CONTAINER_RUNTIME_DOCKER |
| ID | text | Container ID |
| Name | text | Container name |
| Image | Image | The image the container was created from |
| Labels | map(text, text) | Labels attached at creation, carried whole |
| Pod | KubernetesPod | Orchestrator attribution, when the host is managed by one |