# Agent Instructions for pinprick
pinprick is a CLI tool for GitHub Actions supply chain security. It pins action references to full SHAs, checks for updates, and audits pinned actions for runtime fetch patterns that bypass pinning (e.g., `curl ... latest`).
## Project overview
- **Language:** Rust (2024 edition)
- **Platform:** macOS, Linux
- **Architecture:** Single binary CLI with six subcommands (`audit`, `clean`, `completions`, `pin`, `score`, `update`)
- **License:** AGPL-3.0-only
- **Dependencies:** clap/clap_complete (CLI), tokio (async), reqwest (HTTP), serde/serde_norway (parsing), regex (pattern matching), colored (terminal output), toml (config parsing)
## Repository structure
```
pinprick/
├── Cargo.toml
├── build.rs # Embeds audited-actions/ into binary at compile time
├── src/
│ ├── main.rs # Entry point, clap CLI definition, command dispatch
│ ├── audit.rs # Audit command: scan workflows + action source for runtime fetches
│ ├── audit_patterns.rs # Compiled regex patterns for shell/JS/Docker fetch detection
│ ├── audited_actions.rs # Layered lookup: bundled → local cache → remote → GitHub API
│ ├── auth.rs # GitHub token resolution (GITHUB_TOKEN env → gh auth token fallback)
│ ├── config.rs # TOML config file loading (.pinprick.toml, ~/.config/pinprick/)
│ ├── github.rs # GitHub API client (tag→SHA, releases, file trees)
│ ├── output.rs # Human-readable (colored) and --json output formatting
│ ├── pin.rs # Pin command: resolve tags to SHAs, rewrite files
│ ├── score.rs # Score command: compute a posture grade per docs/scoring.md
│ ├── update.rs # Update command: check pinned actions for newer releases
│ └── workflow.rs # Regex-based uses: line scanning, ActionRef types
├── audited-actions/ # Pre-audited action SHAs (bundled into binary)
├── docs/ # Specs (scoring rubric, etc.) — source of truth for behaviors
├── scripts/ # Helper scripts (release notes formatting)
├── site/ # Astro Starlight docs site (pinprick.rs)
├── justfile # Task runner (build, test, lint, check)
├── rustfmt.toml # Rustfmt configuration (2024 style edition)
├── .github/
│ ├── workflows/ # CI, CodeQL, zizmor, release, deploy-site, pinprick-audit, audit-actions
│ ├── dependabot.yml # Dependabot for GitHub Actions, Cargo, and npm
│ └── FUNDING.yml
└── .gitignore
```
## Project-specific notes
### Commands
- `pinprick pin [PATH] [--write]` — Scan `.github/workflows/*.yml`, resolve action tag refs to full SHAs via GitHub API. Dry-run by default (exits 1 when there are unpinned actions). `--write` rewrites files with `@sha # tag` format. Skips already-pinned (SHA) refs. Warns on branch refs (`@main`) and sliding tags (`@v4`), resolving sliding tags to exact versions.
- `pinprick update [PATH] [--write] [--only PATTERN]` — Check SHA-pinned actions for newer releases. Dry-run by default, `--write` to apply changes. `--only` restricts the check to actions whose `owner/repo` contains the given substring.
- `pinprick audit [PATH] [--verbose] [--sarif]` — Scan for runtime fetch patterns that bypass pinning. Without a GitHub token, scans local `run:` blocks and local actions referenced with `uses: ./...`. With a token, also fetches and scans remote action source code (JS/TS, Python, Dockerfiles, action.yml). `--verbose` shows allowed matches. `--sarif` outputs SARIF 2.1.0 for GitHub code scanning.
- `pinprick score [PATH] [--html]` — Compute a supply-chain posture score (0–100, letter grade A–F) for a repository's workflows. Implements the public rubric in `docs/scoring.md` (rubric v0.7.0). The offline rules (`pin.*`, `workflow.*`, `source.unverified`, `runtime.*`) need no token; with a token it additionally emits the token-gated `source.archived` and `source.advisory` rules. `runtime.*` rules reuse the `audit` shell pipeline against each workflow's `run:` blocks, distinguishing pipe-to-shell (-20) from severity-graded fetches (-15/-8/-3). `source.unverified` is an informational zero-point note — publisher outside the trusted baseline (`actions`, `github`) and the `trusted-owners` list in `.pinprick.toml`; it never affects the score or the gate. Exits 1 when any finding deducts points — zero-point informational notes never gate (matches `audit` for CI gating); outputs JSON with `--json` or a self-contained HTML report with `--html` (mutually exclusive with `--json`).
- `pinprick clean` — Remove locally cached audit results (`~/.cache/pinprick/audited/`).
- `pinprick completions <SHELL>` — Generate shell completions for bash, zsh, fish, etc.
### Global flags
- `--json` — Output as JSON for CI integration
- `--color auto|always|never` — Control color output
- `--version` / `-V` — Print version
### YAML handling
**Critical design decision:** workflow files are never round-tripped through a YAML parser for writing. `uses:` lines have a rigid single-line format — regex capture groups replace the ref while preserving leading whitespace, indentation, and surrounding comments. `serde_norway` is only used for read-only extraction of `run:` block contents during audit.
### GitHub auth
1. `GITHUB_TOKEN` environment variable (checked first)
2. `gh auth token` CLI fallback
3. Graceful degradation: `pin` and `update` require a token; `audit` works without one (reduced coverage)
Rate-limit handling: `github::get` retries once on network/5xx errors and sleeps through `x-ratelimit-reset` when the reset is within 60 s; longer waits bail with `RateLimit`.
### Configuration
A `.pinprick.toml` at the repo root (or `~/.config/pinprick/config.toml`) customizes behavior. Keys are all optional: `severity`, `fetch-remote`, `trusted-hosts`, `trusted-owners`, `extra-data-formats`, `ignore.actions`, `ignore.patterns`. Per-repo wholly overrides global (no field-level merge). Because the scanned repo's own config applies, `audit`/`score` print a stderr notice whenever a repo-local config suppressed findings or extended trust, and accept `--no-repo-config` to ignore the repo's file (for scanning repositories you don't control).
### Audit patterns
Six categories of runtime fetch detection:
- **Pipe-to-shell:** `curl`/`wget` piped into `sh`/`bash`/`python`, `bash <(curl …)` process substitution, `bash -c "$(curl …)"` / `eval "$(…)"` command substitution, PowerShell `iex (iwr …)` / `Invoke-Expression (… DownloadString …)`. Flagged high severity regardless of URL versioning.
- **Shell:** `curl`/`wget`/`gh release download` with unversioned URLs, `git clone` without a pinned ref, `go install @latest`, unpinned `pip`/`npm`/`cargo install`/`gem install` installs
- **PowerShell:** `Invoke-WebRequest`/`iwr`/`Invoke-RestMethod`/`irm` with unversioned URLs
- **JavaScript:** `fetch()`/`axios`/`got`/`http.get` with unversioned URLs, `exec()`/`child_process` shelling out to curl
- **Python:** `urllib.request.urlopen`/`requests.get` with unversioned URLs, `subprocess` shelling out to curl/wget
- **Docker:** `FROM :latest` or no tag, `curl`/`wget` in `RUN` instructions (escalated to high when piped to a shell), `ADD` with an `http(s)://` URL source (subject to versioning + data-format exemption via the URL-check path)
Pipe-to-shell pre-empts the other shell/Docker patterns so each line emits a single finding. It also reuses the existing `ShellFetch` SARIF category/rule id to keep downstream configs stable.
URL "versioned" heuristic: a URL is considered versioned if any path segment matches `v?\d+(\.\d+)+`.
Data-format exemption: unversioned-URL rules (shell, JS, Python) do **not** fire when the URL's path ends in a data-format extension (`.json`/`.jsonl`/`.ndjson`, `.yaml`/`.yml`/`.toml`, `.csv`/`.tsv`/`.xml`, `.txt`/`.md`/`.rst`). Matches are recorded as allowed (visible under `--verbose`) with reason `data format URL`. Applies only to the unversioned-URL rules — `/latest/` URLs, pipe-to-shell, and `gh release download` without a tag still fire regardless of extension. `.html` and `.svg` are intentionally excluded because both can carry embedded scripts.
Piped-to-jq exemption: an unversioned-URL fetch whose line pipes into `jq` is recorded as allowed with reason `piped to jq` — the same data-not-code rationale as the data-format exemption, but for JSON API endpoints that carry no file extension (e.g. `curl …/api/v1/crates/<x> | jq …`). The `jq\b` match keeps `jqfoo` from qualifying. Pipe-to-shell matches and pre-empts the URL rules, so `curl … | jq … | bash` is flagged high, never exempted.
Checksum verification: findings followed within 3 lines by `sha256sum`, `shasum`, `openssl dgst`, `gpg --verify`, or `Get-FileHash` are suppressed and recorded as allowed matches (visible under `--verbose`) — the checksum deterministically detects a tampered download, mirroring the SHA-checkout suppression. Pipe-to-shell findings are exempt — the piped payload is never written to disk, so a nearby checksum command cannot verify it.
Git clone ref pinning: `git clone` without `--branch`/`-b` or with a branch name (main, develop, feature/foo) is flagged medium severity. `--branch v1.2.3` (version-like ref) suppresses the finding. A `git checkout <40-char-SHA>` within 3 lines fully suppresses the finding (recorded as allowed, visible under `--verbose`), since the SHA checkout deterministically pins the repo content.
### Exit codes
- `0` — clean (no findings, no pending updates)
- `1` — findings present (audit) or updates available (update dry-run)
- `2` — error
### CI workflows (`.github/workflows/`)
- **audit-actions.yml** — Weekly scan of tracked actions for new releases, automated PRs for clean entries
- **bump-cargo-tools.yml** — Weekly check for newer versions of the cargo-installed CI lint tools (typos, cargo-deny, cargo-nextest, cargo-llvm-cov); opens a PR bumping the pins in ci.yml
- **ci.yml** — Dynamic PR checks: conventional commits, clippy + rustfmt + typos, cargo test, coverage, site format + build, audited-actions verification, and a separate zizmor job with `security-events: write`
- **codeql.yml** — CodeQL security analysis (actions queries) on push to main
- **deploy-site.yml** — Build and deploy Astro site to Cloudflare Workers
- **link-check.yml** — Weekly lychee broken-link check across the built site and README
- **pinprick-audit.yml** — Run pinprick audit on its own workflows with SARIF upload
- **release.yml** — Manual dispatch: dry-run crate publishing, build cross-platform binaries (linux-amd64 and linux-arm64, each in glibc and static musl variants, plus darwin-arm64), create GitHub release with build provenance attestations, publish the crate to crates.io, bump the Homebrew cask (glibc/macOS only)
- **zizmor.yml** — GitHub Actions security audit on push to main
## Safety / do-not-touch rules
1. Do not round-trip workflow files through a YAML parser when writing pins or
updates; preserve the single-line `uses:` replacement model.
2. Keep repo-local config suppressions visible on stderr, and preserve
`--no-repo-config` for scanning repositories the caller does not control.
3. Treat remote action source as untrusted input. Audit may inspect fetched
JavaScript, Python, Docker, and action metadata, but it must not execute
fetched action code.
4. Keep SARIF rule IDs stable when refining detections so downstream code
scanning configuration keeps working.
## Required checks
- `cargo clippy` with zero warnings
- `cargo fmt` for formatting
- No unnecessary abstractions — flat module structure, no nested directories
- `thiserror` for typed errors in library code, `anyhow` for context-rich error propagation in commands
- `LazyLock` for compiled regex constants
## Commit and PR conventions
- Conventional Commits format: `type(scope): description`
- Common types: `feat`, `fix`, `refactor`, `docs`, `ci`, `chore`
- Sign off every commit with `git commit -s` for DCO (enforced by the `.githooks/commit-msg` hook — run `just install-hooks` once per clone to enable it).
- When authored with an AI coding agent, add a `Co-Authored-By` trailer after `Signed-off-by`, naming the agent and model. Current examples: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>` or `Co-Authored-By: Codex GPT-5 <noreply@openai.com>`. Bump the model version as newer ones ship.
### Git workflow
- Never commit directly to `main`; create a feature branch and open a PR.
- PR descriptions should contain only a concise summary of changes. Do not add
test-plan sections, bot attribution, or generated-with footers.