prec is a Linux command execution observability tool where precd uses eBPF to continuously collect process execution events as JSON Lines and prec lets you quickly search and inspect those records from the CLI.
Implementation note:
- Go packages under
internal/are private implementation details and are not a supported public import API
- Does not rely on shell history or
LD_PRELOAD - Captures external command execution from kernel events
- Stores structured logs with process, user, parent process, and tty context for fast triage
- Logs command execution via
WebShellon web servers andOS Command Injectionattacks across various servers - Logs execution traces of
Local Privilege Escalation: LPEattacks that are often missing from application logs - Host-side
precdalso logs commands executed insidedocker,runc, and other containers on the same host - Supports
auditworkflows with searchable records of who ran what, when, where, and with which result - Supports
forensicinvestigations by reconstructing execution timelines fromproc_start,proc_end, andproc_failrecords
Both tools can observe command execution. prec provides normalized, command-centric records with exit_status and proc_fail for quick inspection and timeline reconstruction.
Use prec for command-focused monitoring. Use it alongside auditd when broader syscall or network auditing is also required.
- Target is external commands and selected regular file activity
- Shell builtins such as
cd,export, andaliasare not captured - File monitoring covers successful write-family operations and, when explicitly bounded by
capture_filter, read-only opens to regular files - Broad host auditing needs such as full syscall or network auditing are out of scope
Raw record (precd) |
CLI row (prec) |
Meaning |
|---|---|---|
proc_start + proc_end |
proc_command |
Joined by event_id |
proc_fail |
proc_fail |
Failed execve or execveat |
proc_loss |
proc_loss |
Dropped perf-ring samples |
file_open |
file_open |
File opened with tracked intent |
file_save |
file_save |
Successful write-family activity |
proc_start fields:
timestamp,event_iduid,gid,user,groupauid,session_idpid,ppid,comm,exe,cwdargv,argccgroup,tty,tty_nrsourceparent_comm,parent_exe,parent_cmdline,parent_tty,parent_tty_nr
proc_end fields (compact):
timestamp(end time),event_id,pidduration_nsexit_statussource- unrelated proc_start-only fields are omitted from
proc_endJSON records
Additional fields:
| Record | Additional fields |
|---|---|
proc_fail |
exec_errno, exec_error |
proc_loss |
lost_samples, lost_samples_total |
file_open, file_save |
file_path, file_fd, file_bytes, file_flags, file_dev, file_ino |
Collection details:
argv[0]is normalized to a full path when possible.event_idis theprecdstart time (YYYYMMDDhhmmss) plus a sequence number.timestampis converted from kernel monotonic time to RFC3339Nano in userspace.auidandsession_idcome from/proc/<pid>/loginuidand/proc/<pid>/sessionid.proc_startis written after exec succeeds;proc_endwhen the process exits.exit_statususes the shell-visible range0-255.proc_lossnotifications are aggregated and logged at most once per second.- File events are disabled by default. Enable
collect_file_openand/orcollect_file_saveinprecd.conf. - File events are collected from
openatand write-family syscalls (write,pwrite64,writev,pwritev,pwritev2). By default, onlysource=useractivity is recorded; setcollect_file_sourceto change the scope. file_openrecords successful opens of regular files with write intent (O_WRONLY,O_RDWR,O_CREAT,O_TRUNC,O_APPEND,O_EXCL). Read-only opens requirecollect_file_open = trueand a non-emptycapture_filterto limit perf-ring load.file_saverecords writes to regular files.file_devandfile_inoidentify the target. Writes byprecdto its ownlog_pathare excluded.- Repeated
file_saveevents for the same process and path are suppressed forfile_save_dedup_secseconds (default: 1). collect_file_scope = "user_visible"is the default. It excludes common hidden app-state paths and deleted temporary files.- The log writer syncs approximately once per second, on close, and during rotation.
| Event | source=user condition |
|---|---|
| Command | Interactive tty (/dev/pts/*, /dev/tty, or tty_nr != 0) and an immediate shell parent. Parent tty is used when child tty data is unavailable. |
| File activity | Command condition above, child processes launched from an interactive user shell, non-interactive user shell builtins, or VS Code Remote SSH user processes such as .vscode-server node / extension host. |
All other events are source=system.
Install a release package from Releases, or use the RPM repository setup below on RHEL-compatible distributions.
Run package installation, manual system installation, and service management commands as root.
For rpm and deb upgrades (for example with dnf update or apt upgrade), package post-install scripts run systemctl restart precd.service automatically.
For rpm and deb uninstall actions, package post-remove scripts run systemctl stop precd.service automatically.
Both package and manual installs use /etc/logrotate.d/prec with daily rotation and rotate 7305 (about 20 years). Adjust rotate to suit your retention policy.
Install dnf-plugin-anyrepo, import the RPM GPG key, add this repository, then install the package with dnf.
rpm --import https://raw.githubusercontent.com/jfut/prec/refs/heads/main/packaging/RPM-GPG-KEY-jfut-github
dnf-anyrepo add https://github.com/jfut/prec
dnf install precUpgrade the installed package with dnf.
dnf upgrade precUse one release package downloaded from Releases, or build local package files first:
just releaseInstall one package with your package manager. For downloaded release packages, replace the dist/... path with the downloaded file path.
Use the matching architecture package name (for example, arm64 or aarch64 on ARM64 hosts).
# Debian/Ubuntu
dpkg -i dist/prec_*_amd64.deb
# RHEL/Fedora
rpm -Uvh dist/prec-*.x86_64.rpm
# Alpine
apk add --allow-untrusted dist/prec_*_x86_64.apk
# Arch Linux
pacman -U dist/prec-*-x86_64.pkg.tar.zstBuild the binaries first:
just buildInstall the binaries:
install -m 0755 dist/prec /usr/bin/prec
install -m 0755 dist/precd /usr/sbin/precdInstall config and service files manually:
mkdir -p /etc/prec/precd.d
chmod 750 /etc/prec /etc/prec/precd.d
install -m 0640 packaging/precd.conf.example /etc/prec/precd.conf
mkdir -p /var/log/prec
chmod 0750 /var/log/prec
install -m 0640 packaging/systemd/precd.service /usr/lib/systemd/system/
install -m 0640 packaging/logrotate/prec /etc/logrotate.d/precsystemctl daemon-reload
systemctl enable precd.service
systemctl start precd.service
systemctl status precd.serviceDefault config path: /etc/prec/precd.conf
See: packaging/precd.conf.example
Additional files are loaded with:
include = "/etc/prec/precd.d/*.conf"| Rule | Behavior |
|---|---|
| Path | TOML-quoted file path or glob; relative paths use the including file's directory. |
| Order | Main file first, then matches in lexical order. Nested includes are loaded before the next match. |
| Overrides | Later scalar values win, including false and 0. Omitted settings keep their previous values. |
| Rule arrays | filter and capture_filter arrays append in load order; empty arrays do not clear existing rules. |
| Defaults | Compression defaults and validation are applied after all files are merged. |
| Missing matches | Patterns with no matches, including nonexistent literal paths, are ignored. |
| Errors | Invalid patterns, unreadable or malformed files, and include cycles fail loading. |
The packaged config includes /etc/prec/precd.d/*.conf. Restart precd after config changes.
| Check | Behavior |
|---|---|
| Lock | precd takes an exclusive flock on <log_path>.lock; an existing lock makes startup fail. |
| Paths | Log and lock paths cannot contain symlinks. Parent directories must be owned by root or the current user and not writable by others. Root-owned sticky directories are allowed. |
| Targets | Log and lock files must be single-link regular files owned by precd. |
compress |
Format |
|---|---|
no |
Plain JSONL |
gz |
Gzip JSONL stream |
zstd |
Zstandard JSONL stream (default) |
| Setting | Behavior |
|---|---|
collect_file_open = true |
Trace openat for file_open. With a non-empty capture_filter, matching read-only opens are included; keep this filter narrow (for example, by UID, GID, or comm). |
collect_file_save = true |
Trace write-family syscalls for file_save. |
collect_file_source |
user (default) records user activity; system records system/daemon activity; any records both. |
collect_file_scope |
user_visible (default) excludes common hidden app-state paths; home limits events to the user's home; all includes tracked regular-file paths. |
file_save_dedup_sec |
Suppress repeated records for the same pid and file_path during this window. Default 1; 0 disables. Must be non-negative and fit in time.Duration. |
capture_filter is an optional eBPF allowlist evaluated before events reach the perf ring. It reduces kernel and ring load; filter still applies final user-space filtering.
| Rule | Behavior |
|---|---|
| Syntax | Every rule starts with +. Rules are ORed; && clauses within a term are ANDed. Matching events are captured once; other events are dropped. |
| Included files | Arrays append; capture_filter = [] does not clear existing rules. |
| Fields | uid, gid, pid, user, group, and exact comm. Numeric fields support =, !=, >, >=, <, <=; names and comm support = and !=. |
| Unavailable fields | Fields such as exe, argv, file_path, record_type, and source are rejected at startup. |
| Groups | gid and group match the process GID; supplementary groups are not checked. |
| Refresh | Names resolve at startup and every capture_filter_refresh_sec seconds (default 3600). The value must be positive and fit in time.Duration. Set 21600 for six hours. |
| Account changes | User/group additions, removals, and UID/GID changes take effect at the next refresh. Numeric uid/gid rules do not change. Restart precd to change rules or the refresh interval. |
| Missing names | Missing accounts never match user=/group= and always match user!=/group!=. |
| Lookup errors | Errors fail startup. Refresh errors are logged, retain the previous IDs, and retry at the next interval. |
| Process tracking | Matching processes are tracked before samples are sent. Exit status and exit notifications remain available after account changes; tracking ends when the thread-group leader exits. Untracked processes do not bypass the filter. |
| Limits | Up to 4096 distinct user/group names and 1,048,576 tracked processes. Repeated names of the same kind share one entry. If process registration fails, new samples for that process are dropped. |
Each successful name refresh also invalidates cached user/group names and home directories, including cached missing-account results.
The packaged config captures:
- UID or GID
0, and UID or GID>=1000(conditions are independent;gid=0also matches non-root UIDs) - Matching users or groups:
apache,nginx,php-fpm,www-data,tomcat,jboss,app,admin,www,redis,git,gitlab-www,gitlab-runner,jenkins,grafana,zabbix,rundeck
Missing service accounts do not match equality rules. With no config or capture rules, all users are captured.
The packaged filter excludes repetitive VS Code Server Extension Host system scans: socket checks (ls -l /proc/.../fd/...) and root-process checks (ps -F -A -l | grep root).
To capture all users, add this rule to /etc/prec/precd.d/90-capture-all.conf and restart precd:
capture_filter = ["+uid>=0"]Example:
# Capture events from interactive users and selected web-server workers.
collect_file_open = true
collect_file_source = "any"
capture_filter_refresh_sec = 3600
capture_filter = [
"+uid>=1000",
"+user=apache",
"+user=nginx",
"+comm=php-fpm",
]| Setting | Behavior |
|---|---|
lost_samples_action = "log" (default) |
Write proc_loss records. |
lost_samples_action = "ignore" |
Skip proc_loss records. |
lost_samples_action = "stop" |
Write one proc_loss record, then stop precd. |
| Setting | Behavior |
|---|---|
filter_default |
allow or deny for events not matched by a rule. |
filter |
Ordered first-match rules: +query allows, -query denies. Syntax matches prec --query. |
| Included files | Rules append after main-file rules; empty arrays do not clear rules. Main-file rules are evaluated first. |
| Invalid config | Missing +/- prefixes and legacy include_* / exclude_* keys fail startup. |
| Case | Behavior |
|---|---|
| Source | Defaults to user; --query keeps the selected source unless --source any is set. |
| Default fields | timestamp user group command; with --file-events: timestamp user group record_type file_path file_fd file_bytes command. |
| Command rows | proc_start and proc_end are joined by event_id into one proc_command row. |
| File rows | Hidden by default. --file-events shows independent file rows; file_save is the default. Query record_type=file_open to show open intent. |
| Text / JSON | Text omits empty optional fields; JSON omits unavailable file fields on process rows. |
| Escaping | Text and CSV escape control characters to protect row and terminal structure. |
precd -h:
Usage: precd [flags]
Flags:
-h, --help Show context-sensitive help.
-c, --config=STRING Path to config file (default: /etc/prec/precd.conf)
--version Show version and build info
prec -h:
Usage: prec [flags]
Flags:
-h, --help Show context-sensitive help.
-i, --input=STRING Read log file path (default: /var/log/prec/prec.log)
-a, --all-logs Read current and rotated log files together in list
mode and follow initial output
-s, --source="user" Select source: user,system,any (default: user)
-q, --query=QUERY,... Filter expression, repeatable. Clause format: key op
value, op is = != > >= < <= ~= !~=. Use && for AND,
|| for OR
-f, --fields=STRING Select output fields, comma-separated.
Use + to add and - to remove. Supported:
all,timestamp,end_timestamp,event_id,user,group,
command,uid,gid,auid,session_id,pid,ppid,comm,
exe,cwd,argv,argc,cgroup,tty,tty_nr,source,
record_type,exit_status,duration_ns,duration,
exec_errno,exec_error,lost_samples,
lost_samples_total,parent_comm,parent_exe,
parent_cmdline,parent_tty,parent_tty_nr,
file_path,file_fd,file_bytes,file_flags,
file_dev,file_ino
--full-time Print full RFC3339Nano timestamp
-n, --limit=0 Max rows in list mode. In follow mode, this sets
the number of initial rows before following. A limit
of 0 means no limit in list mode and no initial rows
in follow mode.
-F, --follow Follow command events
--file-events Include file events in CLI output
(requires `collect_file_open = true` and/or `collect_file_save = true` in `precd`)
--tree Print command lineage as a tree
-o, --output=STRING Output format: text,json,csv (default: text)
--version Show version and build info
| Mode | Selection |
|---|---|
| List | Default |
| Follow | --follow or -F |
In follow mode:
proc_startprints a provisional row.proc_endis read only when output or query needsend_timestamp,duration_ns,duration, orexit_status.- When
proc_endis read, a finalized row is printed for the sameevent_id, then join state is released. file_saverows stream independently and do not use the process join.
| Option | Additional behavior |
|---|---|
--all-logs |
Detects gzip and zstd by content and unwraps nested layers. Skips symlinks and non-regular files. In follow mode, rotated logs are used only for initial backfill; live follow stays on the base path. |
--version |
Prints version, commit, date, builtBy, and treeState. |
- Default output is
text. --treerequires list mode and cannot be combined with--file-events.
--follow supports compress = "gz" and compress = "zstd" and follows rotation like tail -F.
Syntax:
--query "key op value"
--query "cond1&&cond2||cond3"
| Value type | Operators |
|---|---|
| Numeric, timestamp, duration | = != > >= < <= |
String, argv text |
= != ~= !~= |
Rules:
&&means AND;||means OR. AND has higher precedence. Repeated--queryoptions are ANDed.- String matches are case-sensitive. Escaping
&&or||inside values is unsupported. sourceacceptsuserorsystem.- Query
record_typevalues are displayed rows:proc_command,proc_fail,proc_loss,file_open, andfile_save. File rows require--file-events; rawproc_startandproc_endvalues are not queryable. timestampandend_timestampaccept RFC3339 or RFC3339Nano.durationusesYYYY-MM-DD HH:MM:SS; conversion is fixed at 365 days per year and 30 days per month. Example:0001-02-03 04:05:06is 1 year, 2 months, 3 days, 4 hours, 5 minutes, and 6 seconds.end_timestamp,exit_status,duration, andduration_nsmatch finalized command rows only.
Keys include every field in each record plus derived command and duration.
| Type | Keys | Match behavior |
|---|---|---|
| Numeric | uid, gid, auid, session_id, pid, ppid, argc, tty_nr, exit_status, duration_ns, exec_errno, lost_samples, lost_samples_total, parent_tty_nr, file_fd, file_bytes, file_flags, file_dev, file_ino |
Numeric operators |
| Timestamp | timestamp, end_timestamp |
RFC3339 comparison |
| Duration | duration |
Duration operators |
| Array | argv |
String-match joined text |
| Other fields | All remaining keys | String operators |
| Group | Fields |
|---|---|
| All | all |
| Identity and time | timestamp, end_timestamp, event_id, user, group, uid, gid, auid, session_id |
| Command | command, pid, ppid, comm, exe, cwd, argv, argc, cgroup, tty, tty_nr, source |
| Result | record_type, exit_status, duration_ns, duration, exec_errno, exec_error, lost_samples, lost_samples_total |
| Parent | parent_comm, parent_exe, parent_cmdline, parent_tty, parent_tty_nr |
| File | file_path, file_fd, file_bytes, file_flags, file_dev, file_ino |
| Selection | Behavior |
|---|---|
No -f |
Use default fields. |
| Plain field names | Output only those fields, in the specified order. |
+field / -field |
Start with defaults, then add or remove fields. |
all |
Expand to all fields. |
Examples:
- Explicit fields:
-f timestamp,uid,gid,group,command - Add and remove:
-f +uid,gid,group,-timestamp,user - Exclude several fields:
-f all,-end_timestamp,event_id,duration_ns,duration,cgroup,tty,tty_nr,source,parent_comm,parent_exe,parent_cmdline,parent_tty,parent_tty_nr,file_path,file_fd,file_bytes,file_flags,file_dev,file_ino - Query failures:
prec -s any --query "record_type=proc_fail" -f timestamp,auid,session_id,exe,exec_errno,exec_error - Query file writes:
prec --file-events -s any --query "record_type=file_save&&file_bytes>=4096" -f timestamp,user,record_type,file_path,file_fd,file_bytes
| Task | Command |
|---|---|
| Recent user commands | prec |
| Follow with 10 initial rows | prec -F -n 10 |
| Follow as CSV | prec -F -n 10 -f all -o csv |
| Follow as JSON | prec -F -n 10 -f all -o json |
| UID range, excluding one UID | prec -q "uid>=1000&&uid!=1999" |
| Commands by a specific user | prec -s any -q "user=user1" |
curl after a timestamp, with result fields |
prec -q "exe~=curl" --query "timestamp>=2026-01-01T00:00:00+09:00" -f +duration,exit_status |
| Commands lasting at least 10 minutes | prec -q "duration>=0000-00-00 00:10:00" -f timestamp,end_timestamp,duration,uid,gid,user,group,command |
| Selected fields with full timestamps | prec -f timestamp,uid,gid,command --full-time |
| All sources / system only | prec -s any / prec -s system |
| Commands by Apache | prec -s any -q "user=apache" |
just test
just build
just snapshotBuild release artifacts locally:
just releaseGenerated files are stored in dist/:
- tar.gz archives including both
precandprecd - Linux package
precincluding bothprecandprecd:deb,rpm,apk,archlinux checksums.txt
GitHub Actions signs RPM artifacts with the GPG private key stored in RPM_SIGNING_KEY. If the key has a passphrase, store it in NFPM_PASSPHRASE.
- Run
git tag -s vX.Y.Z -m vX.Y.Z. - Run
git push origin vX.Y.Zand wait for the Release to be created. - Edit the created Release.
- Press the
Generate release notesbutton and edit the release notes. - Press the
Update releasebutton.
Apache-2.0
Copyright contributors to the prec project.
Jun Futagawa (jfut)