pkglint
A security-focused linter for Arch Linux PKGBUILDs.
pkglint statically analyzes PKGBUILDs and their install scriptlets — without ever sourcing them — and reports findings about source integrity, build hermeticity, code execution, and persistence patterns, condensed into a letter grade per package. It also reproduces makepkg's own build-breaking metadata checks, so a PKGBUILD that would fail to build is caught (and, where the fix is mechanical, rewritten) before you run makepkg. It is built on a real bash AST (mvdan.cc/sh), so the quoting/line-continuation tricks that evade regex-based scanners don't work here.
$ pkglint ~/pkgbuilds/somepkg
somepkg: grade F, 3 finding(s)
PKGBUILD:16:3: critical [PB304] a network download is piped straight into bash and executed
PKGBUILD:11:1: error [PB101] remote source "http://..." has no checksum (SKIP): the download is never verified
PKGBUILD:24:3: error [PB402] sudo escalates privileges during a build; ...
Install
go install github.com/jmelahman/pkglint@latest
Usage
pkglint [flags] [path ...] # paths are package dirs or PKGBUILD files (default: .)
--format text|json|sarif # output format (sarif = SARIF 2.1.0, for code scanning)
--fail-on SEVERITY # exit 1 at or above: info, warn, error (default), critical, never
--ignore PB105,PB206 # disable rules
--rules # list every rule with its documentation
--fix # apply safe auto-fixes in place
--unsafe-fix # also apply behavior-changing fixes (implies --fix)
--diff # with --fix/--unsafe-fix: show changes instead of writing
--offline # with --fix: skip fixes needing network (e.g. VCS ref resolution)
Suppress a reviewed, intentional finding inline:
# pkglint: ignore=PB204
go build -o "$pkgname" .
The directive covers its own line and the line below it, in the file it appears
in only: an ignore= in an .install scriptlet never affects the PKGBUILD,
or vice versa.
Auto-fixing
--fix rewrites what it can and prints every change; --diff previews without
writing. Fixes come in two tiers:
| Tier | Flag | Rules | What it does |
|---|---|---|---|
| Safe | --fix |
PB103, PB203, PB205, PB705, PB708 | Pin a mutable VCS tag/branch to its current commit (via git ls-remote); append --locked to cargo; delete Go verification-disabling env settings; strip a leading slash from backup entries; wrap a scalar list field (depends=foo) in an array (depends=(foo)) |
| Unsafe | --unsafe-fix |
PB204, PB206–PB209, PB403 | Add -mod=vendor to go build; switch npm install→ci and yarn install→--immutable; append --frozen-lockfile to pnpm/bun install, --no-scripts to composer install, --frozen to bundle install and uv sync; drop setuid/setgid mode bits |
Safe fixes preserve behavior or restore a security default; unsafe fixes are
mechanical but change what the build does, so review them. An inline
# pkglint: ignore= on a finding's line also suppresses its fix. Findings whose
remediation isn't a mechanical rewrite (checksums, .SRCINFO) print a one-line
suggestion (updpkgsums, makepkg --printsrcinfo) instead.
Rules
| Group | Rules | What they catch |
|---|---|---|
| Integrity | PB101–PB113 | SKIP/weak checksums, unpinned VCS sources, unencrypted transports, source/url domain and forge-owner mismatches, DLAGENTS and other makepkg.conf overrides, checksum-count mismatches, missing install scripts, PGP signatures without pinned keys, insecure signature transport, unused validpgpkeys |
| Hermeticity | PB201–PB209 | network access outside prepare(), pip/uv pip without --require-hashes, unlocked cargo/npm/yarn/pnpm/bun/composer/bundler/uv/poetry installs, implicit Go module downloads and mutable @latest refs, disabled checksum databases |
| Execution | PB301–PB309 | top-level code, eval, decode-and-execute, download-and-execute (including eval "$(curl ...)" and source <(wget ...)" variants), /dev/tcp, unresolvable command names, embedded payloads, makepkg-internal function overrides, hidden bidi/zero-width characters |
| Filesystem | PB401–PB405 | writes outside $srcdir/$pkgdir, privilege escalation, setuid files and setcap capability grants, install steps that skip $pkgdir, writes to pacman/dynamic-linker/sudoers config |
| Scriptlets | PB501–PB502 | network access and persistence (crontabs, systemd units, shell profiles, login-capable users) in .install files running as root |
| Consistency | PB601–PB603 | PKGBUILD / .SRCINFO drift, network access in pkgver(), provides/replaces/conflicts claims on core system packages |
| Correctness | PB701–PB710 | makepkg build-breakers: invalid pkgname/pkgver/pkgrel/epoch, backup leading slash, unknown options, provides comparison operators, scalar-vs-array field types, schema variables set inside package(), missing/duplicate/mixed arch |
pkglint --rules prints the full documentation for each.
Grading: any critical → F, any error → D, 3+ warns → C, 1–2 warns → B, otherwise A.
A grade is a static hygiene score, not a malware verdict — it measures how reviewable and reproducible a PKGBUILD is. A low grade means "worth reviewing", never "malicious", and a high grade is not an endorsement. Static analysis cannot catch a malicious upstream release pinned with a perfectly valid checksum.
Report card site
site/ generates a static "AUR Report Card" — grades, per-package finding pages,
a rule reference with a flagged/preferred
example for every check, results.json, and embeddable SVG badges. Findings whose rule
has an auto-fix are tagged with a --fix/--unsafe-fix badge so it's clear at a glance
what pkglint can rewrite for you:
go run ./site -maintainer Jamison -top 500 -out docs
It downloads the AUR metadata dump once a day, fetches package snapshots politely
(throttled, cached by LastModified), and scans everything in-process.
Between runs it also remembers each package's source fingerprints (checksums per URL,
VCS commit pins, pkgver) in .cache/state.json and flags drift: a checksum changing
under an unchanged URL, or a commit pin moving without a version bump — the shape a
hijacked upstream release takes. Drifted packages get a warning box on their page, a ⚠
marker on the index, and a drift array in results.json. This is a stateful,
cross-scan signal, so it lives in the site generator rather than the per-file linter.
The generated site is checked into docs/ and served at
https://jamison.lahman.dev/pkglint/. The Report card site
workflow regenerates it nightly and commits any changes.
Roadmap
- A
makepkgshim so AUR helpers lint before building (yay --makepkg pkglint-makepkg, paru[bin] Makepkg) - Sandboxed builds: containerized
makepkgwith the package artifact installed on the host viapacman -U - Hermetic builds: two-phase
makepkg -o(network) /makepkg -e(--network=none), with these lint rules enforcing the conventions that make that split work
License
GPLv3 — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pkglint-1.0.0.tar.gz.
File metadata
- Download URL: pkglint-1.0.0.tar.gz
- Upload date:
- Size: 132.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a3a117b70e8641c6916856d63af7ca3b015bd54358ce3fbc4593b7edc71bc9b
|
|
| MD5 |
d04d6c08577e750477d6e39bb01f9f49
|
|
| BLAKE2b-256 |
6587a99570065d7c8c695a3dda64c111101f2a8bc728aab67b8cb4cb5757f513
|
File details
Details for the file pkglint-1.0.0-py3-none-manylinux_2_17_x86_64.whl.
File metadata
- Download URL: pkglint-1.0.0-py3-none-manylinux_2_17_x86_64.whl
- Upload date:
- Size: 2.1 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9edf041ba302350d0ef46449d38e830b0c025bc7b25642ae51c7f87ae741d184
|
|
| MD5 |
b2841e2e7d7267113fdbd270e94c1e85
|
|
| BLAKE2b-256 |
f793c4071b6abf5da68cc53c6d28f0ba210aee0c25669f96ce5f4a86cdc197a1
|
File details
Details for the file pkglint-1.0.0-py3-none-manylinux_2_17_aarch64.whl.
File metadata
- Download URL: pkglint-1.0.0-py3-none-manylinux_2_17_aarch64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12e292a6d6fccec356bfc32ebf9e366c60fb87bf062bf398756656f86c71cfa8
|
|
| MD5 |
2ed8dde666a8fb5e1ef1e5975e25656c
|
|
| BLAKE2b-256 |
dbaddc03d1edb1a60288b63485d4bf11bbdc12fc21a57477c13760faf91e8a8e
|
File details
Details for the file pkglint-1.0.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: pkglint-1.0.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b4afea98f1e30b1014411e0bf57bce5a0630d88ed3cf6c175160cb68c0efa91
|
|
| MD5 |
1b11a08468bc985fee0ef8ffc977029d
|
|
| BLAKE2b-256 |
14a361bab50fe802bf56e24e4eeaf6b8a01cf7ce489d6a261b0aab9ee17ec4fb
|
File details
Details for the file pkglint-1.0.0-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: pkglint-1.0.0-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 2.1 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bce4afb84ee0c58ffcd3c9889d34be081601d53e92d28475b2251afd873f1014
|
|
| MD5 |
a71ed372737b950bdcfb498ba1988ad8
|
|
| BLAKE2b-256 |
a3db6b2db4b35f9e53e59e26d62f522ed9375f105cab129ed49a013373de7457
|