Skip to main content

pkglint

A security-focused linter for Arch Linux packages.

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.

Built packages (*.pkg.tar.zst and friends) are first-class inputs too: pkglint inspects the archive the way namcap does — ELF hardening (PIE, RELRO, executable stacks, text relocations, RPATH), stripping and placement, dependencies inferred from linked shared libraries and script shebangs (via pacman's local database), packaged .INSTALL scriptlets, and filesystem hygiene from FHS layout to stale python bytecode — while never executing anything from the package (no ldd, no interpreter launches; ELF files are parsed, not loaded).

$ 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: warn [PB403] chmod 4755 creates a setuid/setgid file

1 package linted: 1 with findings
1 finding(s) fixable with --unsafe-fix

Packages with nothing to report stay out of the way — they are only counted in the closing summary line (--verbose lists them individually). The line after it tallies the findings a rule can rewrite, split by the flag that applies them (--fix for behavior-preserving fixes, --unsafe-fix for the rest). A few fixes also need state the linter does not have — the sources on disk for a digest, an https host that answers — so the tally is what a fix run will attempt, not a promise of that many rewrites.

Install

AUR

yay -S pkglint

PyPi

uv tool install pkglint

Go

go install github.com/jmelahman/pkglint@latest

Github Releases

Prebuilt binaries for Linux and macOS (amd64 and arm64) are attached to every release.

Usage

pkglint [flags] [path ...]     # paths are package dirs, PKGBUILD files,
                               # or built packages (*.pkg.tar.*) (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 (default), error, critical, never
  --ignore PB105,PB206         # disable rules
  --color auto|always|never    # colorize text output (auto = only on a terminal; honors NO_COLOR)
  --verbose                    # list packages with no findings individually, not just in the summary
  --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/--add-ignores: show changes instead of writing
  --offline                    # with --fix: skip fixes needing network (VCS ref resolution, https probing)
  --add-ignores                # insert ignore directives suppressing every current finding
  --no-inline-ignores          # disregard ignore directives (audit an untrusted package)

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. --add-ignores writes these directives for you, one per finding still reported, so a package can adopt pkglint without first fixing (or while deliberately keeping) what it flags; --diff previews the insertions.

Directives are audited, not just obeyed. A directive that no longer matches a finding on its line or the next — the issue was fixed, or the ID was never a pkglint rule — is itself flagged (PB913, stale-ignore-directive), and --fix deletes it, so a fixed issue cannot leave behind a comment that would silence its return. And when reviewing a package you don't trust, --no-inline-ignores disregards every directive and reports whatever the maintainer suppressed.

Auto-fixing

--fix rewrites what it can and prints every change; --diff previews without writing. Fixes come in two tiers:

Safe — --fix

Rules What it does
PB102 Add sha256sums beside a weak md5sums/sha1sums, computed from sources already on disk (see below)
PB103 Pin a mutable VCS tag or branch to the commit it names right now (via git ls-remote)
PB205, PB916 Delete Go verification-disabling env settings; insert -modcacherw so the module cache stays removable
PB705, PB708 Strip a leading slash from backup entries; wrap a scalar list field (depends=foo) in an array
PB711, PB933, PB944, PB952, PB954, PB979 Declare in makedepends a build tool the build already invokes: the VCS client a source needs, python-build/python-installer, rust, cmake, meson, npm
PB904, PB912, PB918, PB919 Delete dependency metadata the rule proves does nothing: a makedepends/optdepends entry already in depends, a package naming itself in provides/conflicts
PB913 Remove stale ignore directives

Unsafe — --unsafe-fix

Rules What it does
PB104, PB112 Upgrade an insecure source transport (http:///ftp://https://, bare git://git+https://), signature sources included, and only after a headers-only request (or git ls-remote) confirms the https URL answers, so an unserved one keeps the finding — reachable still isn't identical, so rebuild after
PB203, PB204, PB206–PB209 Pin a build to its lockfile: --locked on cargo (fails the build if no Cargo.lock ships), a go mod download prepare() step, npm installci, yarn install --immutable, --frozen-lockfile on pnpm/bun install, --no-scripts on composer install, --frozen on bundle install and uv sync
PB914, PB915, PB917 Restore Arch's Go build flags: insert -buildmode=pie and -trimpath, and export CGO_CFLAGS/CGO_LDFLAGS so the hardening flags reach C code
PB940, PB941, PB942 Cargo profile and install flags: drop --release from cargo test/cargo check so the suite runs with debug assertions and overflow checks on, insert it into a build() cargo build so the package ships the optimized profile, append --no-track to cargo install
PB950, PB951, PB953, PB980 Point a build system at Arch's layout: -DCMAKE_INSTALL_PREFIX=/usr, -DCMAKE_BUILD_TYPE=None, --prefix=/usr on meson setup, and an npm --cache inside $srcdir instead of the invoking user's ~/.npm
PB961, PB973, PB981 Add installed-runtime metadata the guidelines require: the provides/conflicts pair a -git package owes its release counterpart (written together or not at all), dkms, java-runtime
PB931, PB971, PB974 Delete a declared dependency the rule calls unnecessary: a pytest lint plugin from checkdepends, every depends entry of a font package, a -dkms package's pinned linux*-headers
PB403 Drop setuid/setgid mode bits
PB902 Prefix a custom variable with an underscore (pyname_pyname), declaration and every reference at once

Safe fixes preserve behavior or restore a security default; unsafe fixes are mechanical but change what the build does, so review them. Every fix stands down where it cannot see the whole picture: the --release insertion declines when the PKGBUILD reads a debug/ artifact path (since --release moves the binary) or when the command's flags arrive through an expansion; the checkdepends removal declines while a check() command still passes the plugin's flag, which would leave a check phase that cannot run; and the dependency-array edits decline an array they cannot re-render faithfully — one a later += extends, an entry written as a brace group standing for more than one name, an array assigned by top-level control flow. Two rules reaching for the same absent array — a Rust build whose sources are also a git checkout — are folded into one assignment rather than each writing its own. The PB902 rename is all-or-nothing: it lands only when every occurrence of the name is accounted for, and it stands down entirely when the variable is exported (a build tool may read it from the environment), when the underscored spelling is taken, or when the file reaches variables in ways no rewrite can follow — run-time naming (eval, ${!x}, declare -n), source, a reference spelled in literal text such as a trap string — because a half-applied rename would leave the PKGBUILD reading a variable nothing sets. An inline # pkglint: ignore= on a finding's line also suppresses its fix. Findings whose remediation isn't a mechanical rewrite print a one-line suggestion instead: updpkgsums for checksums, makepkg --printsrcinfo for a stale .SRCINFO. Those suggestions are computed from what is left after fixing, so a checksum --fix repaired is not then nagged about.

The PB102 fix is the one whose remedy is data rather than syntax, so it applies only where it can prove what it writes. It hashes sources already downloaded into the package directory or $SRCDEST — pkglint never fetches a source, since that would mean issuing requests to URLs read out of an untrusted file — and it emits a digest only after re-computing the existing md5/sha1 from the same read and finding it matches. The sha256sums it adds therefore covers bytes the weak digest already vouched for: replacing them would take an md5 preimage, not the collision that makes md5 unfit for new use. Sources that aren't present, a digest that doesn't match, or an array pkglint can't pair index-for-index all leave the finding standing, and updpkgsums remains the way to close it. The weak array is kept — makepkg checks every array present, so the edit is purely additive.

Commit hook

This repo ships hooks for any runner that understands the .pre-commit-config.yaml convention, so a packaging tree can lint its PKGBUILDs on every commit:

repos:
  - repo: https://github.com/jmelahman/pkglint
    rev: v1.3.4
    hooks:
      - id: pkglint

Three hook ids are available: pkglint builds from source with the Go toolchain, pkglint-system runs whatever pkglint is already on $PATH, and pkglint-fix applies the safe auto-fixes in place (offline by default). Tune any of them with e.g. args: [--ignore, PB105, --fail-on, critical].

Rules

Group Rules What they catch
Integrity PB101–PB114 SKIP/weak/malformed 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–PB504 network access and persistence (crontabs, systemd units, shell profiles, login-capable users) in .install files running as root, unparseable scriptlets, commands pacman hooks already run
Consistency PB601–PB603 PKGBUILD / .SRCINFO drift, network access in pkgver(), provides/replaces/conflicts claims on core system packages
Correctness PB701–PB711 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, VCS sources without their client in makedepends
Built package PB801–PB842 everything namcap checks in a .pkg.tar.*: ELF in any packages and nonstandard paths, executable stacks, text relocations, missing RELRO, non-PIE executables, unstripped binaries, insecure RPATH/RUNPATH, missing/unused library and interpreter dependencies (resolved through pacman's database, statically — no ldd), stale soname declarations, pkg-config requirements, FHS layout, permissions and ownership, empty directories, invalid filenames, cross-directory hardlinks, dangling symlinks, .la/perllocal.pod/info dir/MIME-cache landmines, stale python bytecode, site-packages/tests, systemd/D-Bus units under /etc, missing license and backup files, doc-heavy packages, sphinx caches, jars outside /usr/share/java — plus the full scriptlet analysis over the packaged .INSTALL
Style PB901–PB984 namcap's PKGBUILD conventions: hardcoded architectures instead of $CARCH, custom variables without _ prefix, $startdir, redundant makedepends, pinned SourceForge mirrors, pkgname repeated in pkgdesc, makepkg-internal output helpers, missing Maintainer tag, uppercase package names, missing pkgdesc/url/license, version-only download names, depends duplicated in optdepends, stale ignore directives; plus the published Arch package guidelines: self-provides/self-conflicts, pre-SPDX license identifiers, installs into /usr/local or /usr/libexec; the Go guidelines: go build without -buildmode=pie or -trimpath, module caches written read-only (no -modcacherw), CFLAGS/LDFLAGS never forwarded to cgo; the Python guidelines: tox, lint/coverage plugins gating check(), pre-built wheels as sources, the python-build/python-installer flow and its makedepends; the Rust guidelines: cargo test --release, cargo install without --no-track, debug-profile builds, missing rust makedepends; the CMake/Meson guidelines: missing /usr prefix, CMAKE_BUILD_TYPE=Release clobbering Arch's flags, build tools missing from makedepends, bare ninja in meson builds; the VCS guidelines: tip-following sources without pkgver(), -git packages without provides/conflicts, $pkgver in checkout folder names, -git suffixes that don't match the sources; and the per-ecosystem naming families — fonts (arch=('any'), no depends, unstable download hosts), DKMS (dkms in depends, no pinned kernel headers), lib32 ((32-bit) pkgdesc, -m32), MinGW (!strip staticlibs !buildflags, (mingw-w64) pkgdesc), Node.js (npm makedepends, --cache in $srcdir), Java (java-runtime depends), CLR (arch=('any') + !strip), Haskell (never arch=('any')) and PHP (pure-PHP packages are arch=('any'))

pkglint --rules prints the full documentation for each. A full reference including examples of each rule is available in the documentation.

Relationship to namcap

pkglint covers namcap's rule set — both the PKGBUILD checks and the built-package checks — with a few deliberate differences: nothing from the analyzed package is ever executed (namcap runs ldd -r -u on packaged binaries; pkglint compares dynamic symbol tables instead), findings the lint host cannot actually verify (a library owned by a package that isn't installed here, a declared dependency that isn't installed) are reported informationally instead of as hard errors, and everything is folded into the same graded, suppressible, JSON/SARIF-capable reporting the PKGBUILD rules use. Dependency inference reads pacman's local database directly (/var/lib/pacman/local) and degrades gracefully on non-Arch hosts by skipping just those rules.

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.

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

pkglint-1.4.0.tar.gz (381.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pkglint-1.4.0-py3-none-manylinux_2_17_x86_64.whl (4.4 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

pkglint-1.4.0-py3-none-manylinux_2_17_aarch64.whl (4.0 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

pkglint-1.4.0-py3-none-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

pkglint-1.4.0-py3-none-macosx_10_12_x86_64.whl (4.5 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file pkglint-1.4.0.tar.gz.

File metadata

  • Download URL: pkglint-1.4.0.tar.gz
  • Upload date:
  • Size: 381.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","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

Hashes for pkglint-1.4.0.tar.gz
Algorithm Hash digest
SHA256 fadca51bea14b7f0e4b3df4c9c34e55a24427d1a337b9d5a1b32274f8cc0b419
MD5 048a4d9c38582ee8dac6dff00b499eb2
BLAKE2b-256 3d02d787802a8d6c7778cd59f1d30f21f7716f3b68b236d815cfcfb180959072

See more details on using hashes here.

File details

Details for the file pkglint-1.4.0-py3-none-manylinux_2_17_x86_64.whl.

File metadata

  • Download URL: pkglint-1.4.0-py3-none-manylinux_2_17_x86_64.whl
  • Upload date:
  • Size: 4.4 MB
  • Tags: Python 3, manylinux: glibc 2.17+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","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

Hashes for pkglint-1.4.0-py3-none-manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 a79d1d4513bffd403830df69748c3b7872b6610b9f8cec9f448fb32968c18ea8
MD5 a5c5ba67b1d3791558ad49926ec08f18
BLAKE2b-256 d1e5127dfbad59613b83818b1d71ff500c8cbe3c95fb87e1e8e6f770e73ac270

See more details on using hashes here.

File details

Details for the file pkglint-1.4.0-py3-none-manylinux_2_17_aarch64.whl.

File metadata

  • Download URL: pkglint-1.4.0-py3-none-manylinux_2_17_aarch64.whl
  • Upload date:
  • Size: 4.0 MB
  • Tags: Python 3, manylinux: glibc 2.17+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","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

Hashes for pkglint-1.4.0-py3-none-manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 a6510195da8130e26b197b26af327526c7eeba33ee4b02012b10850f34b497ba
MD5 9df8a3fb784e051842a70ec00ec20fa4
BLAKE2b-256 bb3c3e686273f75e5805209bf1c7b1593e66cc0d70d84f8d6d0522752ece7ae8

See more details on using hashes here.

File details

Details for the file pkglint-1.4.0-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: pkglint-1.4.0-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 4.1 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","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

Hashes for pkglint-1.4.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 806b808856a0d6513408d5a4f9c8b31a92056a3fd2743c5879f530a50c68562c
MD5 a7beecec9c00509b2e555e57fc1842e4
BLAKE2b-256 06abe52947d4639089e2655c6ec7b152210aaa0aece1763edae3f9b6c1a7b17c

See more details on using hashes here.

File details

Details for the file pkglint-1.4.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

  • Download URL: pkglint-1.4.0-py3-none-macosx_10_12_x86_64.whl
  • Upload date:
  • Size: 4.5 MB
  • Tags: Python 3, macOS 10.12+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","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

Hashes for pkglint-1.4.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 fc545d7379807894684ce413d5fe5887f203c3711bf0c79c40f1bc2e150a9d45
MD5 06ce9c9a90269aa219b21e68add5d555
BLAKE2b-256 59a88bc554ddb33c5abddf14cef2557fcc007a3c5edfa59daf90d010b58c58b0

See more details on using hashes here.

Release history Release notifications | RSS feed

1.6.2

5 files

1.6.1

5 files

1.6.0

5 files

1.5.3

5 files

1.5.2

5 files

1.5.1

5 files

1.5.0

5 files

This release

1.4.0 This release

5 files

1.3.4

5 files

1.3.3

5 files

1.3.2

5 files

1.3.1

5 files

1.3.0

5 files

1.2.1

5 files

1.2.0

5 files

1.1.3

5 files

1.1.2

5 files

1.1.1

5 files

1.1.0

5 files

1.0.1

5 files

1.0.0

5 files

0.1.0

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page