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
prec and auditd can both observe command execution.
precis focused on command visibility and quick inspection by humansprecoutputs normalized command-centric records directlyprecfocuses on command execution records and includesexit_statusandproc_failpreccan also be used for forensic investigation by reconstructing execution timelines from structured records
In practice, prec is designed for command-focused monitoring and forensic use cases. It can replace parts of auditd workflows that only require command execution visibility, and can also be deployed together with auditd.
- 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 log records from precd:
record_type=proc_startrecord_type=proc_endrecord_type=proc_failrecord_type=proc_lossrecord_type=file_openrecord_type=file_save
Merged record_type values from prec:
record_type=proc_commandrecord_type=proc_failrecord_type=proc_lossrecord_type=file_openrecord_type=file_save
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
proc_fail and proc_loss additional fields:
proc_fail:exec_errno,exec_errorproc_loss:lost_samples,lost_samples_total
file event additional fields:
file_path,file_fd,file_bytes,file_flags,file_dev,file_ino
Notes:
argv[0]is normalized to a full path when possibleevent_idformat isprecdstart timeYYYYMMDDhhmmss+ sequence numberauidandsession_idare read from/proc/<pid>/loginuidand/proc/<pid>/sessionidtimestampis derived from kernel monotonic time and converted to RFC3339Nano in userspaceexit_statusis the shell-visible status code range0-255proc_startis written immediately after exec succeedsproc_endis written when the process exitsproc_failrecords are written whenexecveorexecveatreturns an errorproc_lossrecords are written when the perf ring reports dropped samples- repeated
proc_lossnotifications are aggregated and logged at most once per second - file activity is collected from the
openatand write-family syscall tracepoints such aswrite,pwrite64,writev,pwritev, andpwritev2 - file activity collection is disabled by default and starts only when
collect_file_open = trueorcollect_file_save = trueis set inprecd.conf - by default, file activity records only
source=useroperations; usecollect_file_sourceto switch the target scope file_openis emitted for successfulopenatcalls to regular files with write-capable intent such asO_WRONLY,O_RDWR,O_CREAT,O_TRUNC,O_APPEND, orO_EXCL- read-only
file_openrecords are also emitted whencollect_file_open = trueand a non-emptycapture_filteris configured; the early filter is required to prevent perf-ring overload file_saveis emitted only when the write-family target is a regular filefile_devandfile_inoidentify the device and inode observed for the regular-file targetprecdself-writes to its ownlog_pathare excluded to reduce collector noise- repeated
file_saveevents from the same process to the same file are suppressed withinfile_save_dedup_secseconds (default: 1) collect_file_scope = "user_visible"is the default and keeps user-visible files while excluding common hidden app-state paths and deleted temp files- the log writer syncs data approximately once per second during operation and also syncs during close and log rotation
source=user is assigned only when:
- command has an interactive tty (
/dev/pts/*or/dev/tty, ortty_nr != 0fallback). If child tty data is unavailable, parent tty is used as fallback - immediate parent process is a shell
- for file events, child processes launched from an interactive user shell are also treated as
user - for file events, non-interactive user shell builtins are also treated as
user - for file events, VS Code Remote SSH user processes such as
.vscode-servernode/ extension host are also treated asuser
Everything else is 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.
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.zstPackage installs place /etc/logrotate.d/prec with a default policy of daily and rotate 7305 (about 20 years of retention).
Adjust the rotate value as needed to match your retention policy.
Build 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/precThe installed /etc/logrotate.d/prec default is daily with rotate 7305 (about 20 years of retention).
Adjust the rotate value as needed to match your retention policy.
systemctl 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 configuration files:
include = "/etc/prec/precd.d/*.conf"- configuration uses TOML, so the include path must be quoted
includeaccepts a single file path or glob pattern; relative paths are resolved from the including file's directory- the including file is applied first, then matching files in lexical filename order, regardless of where
includeappears in the file - included files may also use
include; nested includes are applied before the next matching file - later scalar settings override earlier values, including explicit
falseand0 filter = [...]andcapture_filter = [...]in included files append rules in load order instead of overwriting earlier rules; empty arrays add no rules and do not clear earlier rules- omitted settings retain their previous values; compression defaults and validation are applied after all files are merged
- patterns with no matches are ignored, including nonexistent literal paths
- invalid include patterns, unreadable or malformed included files, and include cycles cause loading to fail
- the packaged configuration enables
/etc/prec/precd.d/*.conf; restartprecdafter changing configuration
Single-instance behavior:
precdacquires an exclusiveflockon<log_path>.lockat startup- if the lock is already held, startup fails immediately to prevent duplicate collection
Compression modes:
compress = "no"plain JSONLcompress = "gz"gzip-compressed JSONL streamcompress = "zstd"zstd-compressed JSONL stream (default)
File save dedup:
collect_file_open = trueenablesopenatsyscall tracing forfile_open- with a non-empty
capture_filter,collect_file_open = truealso captures matching read-only opens; keep the filter narrow, for example by UID, GID, orcomm collect_file_save = trueenables write-family syscall tracing and emitsfile_saverecordscollect_file_source = "user"records only user-originated file activity (default)collect_file_source = "system"records only system or daemon file activitycollect_file_source = "any"records both user and system file activitycollect_file_scope = "user_visible"records user-visible files and excludes common hidden app-state paths (default)collect_file_scope = "home"records all files under the user's home directorycollect_file_scope = "all"records all tracked regular-file pathsfile_save_dedup_sec = 1suppress repeatedfile_saverecords for the samepidandfile_pathwithin the configured windowfile_save_dedup_sec = 0disables this suppression
Early capture filter:
capture_filteris an optional positive allowlist evaluated in eBPF before events are sent to the perf ringcapture_filter = [...]in an included file adds to the existing allowlist;capture_filter = []does not clear it- rules and
||terms are ORed;&&clauses within a term are ANDed - every rule must start with
+; a matching event is captured and a non-matching event is dropped - supported fields are
uid,gid,pid,user,group, and exactcommmatches userandgroupnames are resolved to UID/GID whenprecdstarts- numeric fields support
=,!=,>,>=,<, and<=;user,group, andcommsupport=and!= - fields that require event data unavailable at the tracepoint, such as
exe,argv,file_path,record_type, andsource, are rejected at startup capture_filteris a coarse prefilter for reducing kernel and perf-ring load;filterremains the final user-space filter
Example:
# Capture events from interactive users and selected web-server workers.
collect_file_open = true
collect_file_source = "any"
capture_filter = [
"+uid>=1000",
"+user=apache",
"+user=nginx",
"+comm=php-fpm",
]Lost sample actions:
lost_samples_action = "log"writeproc_lossrecords (default)lost_samples_action = "ignore"skipproc_lossrecordslost_samples_action = "stop"write oneproc_lossrecord and stopprecd
Filter rules:
filter_default = "allow" | "deny"controls events that match no rulefilter = ["+query", "-query", ...]uses ordered first-match evaluationfilter = [...]in an included file appends to existing rules;filter = []does not clear them- rules in the main configuration are evaluated before rules from included files, so an earlier matching rule takes precedence
- each rule must start with
+allow or-deny - query expression syntax is identical to
prec --query - if a rule has no
+or-prefix,precdfails to start - legacy
include_*andexclude_*keys are rejected
precwithout options is equivalent to applying--source userprecwith--queryalso applies the selected--sourcefilter unless--source anyis specified- Default output fields are
timestamp user group command - Default output fields with
--file-eventsaretimestamp user group record_type file_path file_fd file_bytes command - human-readable text omits empty optional fields, and JSON omits unavailable file-specific fields from process records
precjoinsproc_startandproc_endbyevent_idand shows one logicalrecord_type=proc_command- file records are hidden by default; use
--file-eventsto include them - file records are not merged and are shown as independent rows when
--file-eventsis set --file-eventsshowsfile_saverecords by default; use--query "record_type=file_open"when open intent is needed--treecannot be used with--file-events
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; initial rows before follow in
--follow mode (0 means unlimited 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
- list mode: default
- follow mode:
--followor-F
Follow mode semantics:
- when
proc_startarrives,precprints a provisional merged row precprocessesproc_endonly when finalized fields are needed by output or query- output fields:
end_timestamp,duration_ns,duration,exit_status - query keys:
end_timestamp,duration_ns,duration,exit_status
- output fields:
- when
proc_endis processed,precprints another row for the sameevent_idwith finalized values and then releases in-memory join state - file save records are streamed as-is with
--file-eventsand do not require proc_start/proc_end join processing
-i,--input: read from specified log path instead of configlog_path-a,--all-logs: include rotated logs in list mode and follow initial backfill- gzip and zstd layers are detected from file content and unwrapped recursively, so gzip-compressed rotated zstd logs are included
-s,--source: select source (user,system,any)- use
anywhen--querycontains customsourcelogic
- use
-q,--query: filter expression, repeatable-f,--fields: output fields selection--full-time: keep RFC3339Nano timestamp text-n,--limit: max rows in list mode, or initial rows before follow--tree: tree view in text list mode only-o,--output: output format selection (text,json,csv)--version: print version and build info (version,commit,date,builtBy,treeState)
- Default output mode is text
--treecannot be used with--follow
--follow works with compress = "gz" and compress = "zstd" logs.
It tracks log rotation similarly to tail -F.
--all-logs nuance in follow mode:
- rotated files are used only for initial backfill (
-n) - live follow continues on the base log file path
Syntax:
--query "key op value"
--query "cond1&&cond2||cond3"
Operators:
- numeric, timestamp, and duration:
= != > >= < <= - string and argv text:
= != ~= !~=
Rules:
&&is AND operator||is OR operator- AND has higher precedence than OR
- repeated
--queryis AND at top level sourcevalue must beuserorsystemrecord_typevalues inprecquery match displayed rows:proc_command,proc_fail,proc_loss,file_open, orfile_save- file
record_typequeries require--file-events; otherwise file records are excluded before user queries are applied - raw log-only record types such as
proc_startandproc_endare joined before filtering and are not accepted as query values - string match is case-sensitive
timestampandend_timestampaccept RFC3339 or RFC3339NanodurationacceptsYYYY-MM-DD HH:MM:SSand means elapsedyear-month-day hour:minute:second- example:
0001-02-03 04:05:06means 1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds - conversion rule is fixed to 1 year = 365 days, 1 month = 30 days
- example:
- escaping
&&and||inside values is not supported end_timestamp,exit_status,duration, andduration_nsconditions match only finalized command rows, not provisional rows
Supported query keys:
- all JSON fields in each event record
- derived keys:
command,duration
Type rules:
- numeric/timestamp/duration typed keys keep strict operators:
- 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 - timestamp:
timestamp,end_timestamp - duration:
duration
- numeric:
- array key:
argvis string-matched as joined text - other keys are treated as string fields and support
= != ~= !~=
Supported fields:
alltimestamp,end_timestamp,event_id,user,group,commanduid,gid,auid,session_id,pid,ppidcomm,exe,cwd,argv,argccgroup,tty,tty_nr,source,record_type,exit_status,duration_ns,duration,exec_errno,exec_error,lost_samples,lost_samples_totalparent_comm,parent_exe,parent_cmdline,parent_tty,parent_tty_nrfile_path,file_fd,file_bytes,file_flags,file_dev,file_ino
Selection rules:
- no
-f: default fields - plain tokens only: explicit mode, output only specified fields in that order
- token with
+or-: start from defaults, then add or remove allexpands to all fields
Examples:
-f timestamp,uid,gid,group,command-f +uid,gid,group,-timestamp,user-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_inoprec -s any --query "record_type=proc_loss" -f timestamp,record_type,lost_samples,lost_samples_totalprec -s any --query "record_type=proc_fail" -f timestamp,auid,session_id,exe,exec_errno,exec_errorprec --file-events -s any --query "record_type=file_open" -f timestamp,user,record_type,file_path,file_fd,file_flagsprec --file-events -s any --query "record_type=file_save&&file_bytes>=4096" -f timestamp,user,record_type,file_path,file_fd,file_bytes
Show recent user-origin command executions with default fields.
precFollow mode with 10 initial rows, then keep streaming new events.
prec -F -n 10Follow mode in CSV with all available fields.
prec -F -n 10 -f all -o csvFollow mode in pretty-printed JSON for pipeline analysis.
prec -F -n 10 -f all -o json | jq .Filter by UID range, useful to focus on regular users except a specific service account.
prec -q "uid>=1000&&uid!=1999"Include all sources and show commands run by user1 or root.
prec -s any -q "user=user1||user=root"Show curl executions after a specific time and add duration and exit status.
prec -q "exe~=curl" --query "timestamp>=2026-01-01T00:00:00+09:00" -f +duration,exit_statusFind long-running commands that executed for 10 minutes or more.
prec -q "duration>=0000-00-00 00:10:00" -f timestamp,end_timestamp,duration,uid,gid,user,group,commandPrint selected fields with full RFC3339Nano timestamps.
prec -f timestamp,uid,gid,command --full-timeShow both user and system events.
prec -s anyShow only system events.
prec -s systemMonitor commands executed by common web server accounts, which helps detect command execution caused by OS command injection attacks and possible web shell activity.
prec -s any -q "uid=48 || uid=976 || user=apache || user=nginx"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)