# Pin Everything

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-cboone-agent-harness-plugins-pin-everything`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cboone](https://agentstack.voostack.com/s/cboone)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cboone](https://github.com/cboone)
- **Source:** https://github.com/cboone/agent-harness-plugins/tree/main/dist/codex/plugins/pin-everything/skills/pin-everything
- **Website:** https://github.com/cboone/agent-harness-plugins

## Install

```sh
agentstack add skill-cboone-agent-harness-plugins-pin-everything
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Pin Everything

Pin every version surface in a repository as a one-shot supply-chain hardening pass: SHA pins for actions, integrity hashes for Corepack, exact pins for package-manager dependencies (in application context), version-file refs for language runtimes, and explicit versions for install commands. Optionally generates a weekly drift-audit script and tightens Dependabot.

This skill is a generalization of the canonical hardening pass executed in [PR #250](https://github.com/cboone/agent-harness-plugins/pull/250) on `agent-harness-plugins` itself. Adopters who want to repeat that pass on their own repositories run the skill end-to-end. Adopters who only want a subset (for example, SHA-pinning actions) can stop after the relevant step.

## Workflow

### 1. Audit the Repo for Version Surfaces

Scan the working tree for every version surface, then output a categorized table (file paths, count of refs, current pin state).

Surfaces to detect:

| Category                    | Detection                                                                                                                                                                                                 |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GitHub Actions `uses:` refs | Glob `.github/workflows/*.{yml,yaml}` and `.github/actions/**/action.{yml,yaml}`; also grep `**/*.md` for `uses:` lines (scaffolded reference docs and templates often embed real refs); grep for `uses:` |
| Reusable workflow refs      | Same files plus the same Markdown sweep; grep for `uses:` lines containing `.github/workflows/`                                                                                                           |
| `packageManager` field      | Read `package.json`; check `.packageManager`                                                                                                                                                              |
| `package.json` deps         | Read `package.json`; flag `^`/`~` ranges in `dependencies`/`devDependencies`. **Skip `peerDependencies`** — see step 6.                                                                                   |
| Language version files      | Glob `.tool-versions`, `.nvmrc`, `.node-version`, `.ruby-version`, `.python-version`, `rust-toolchain.toml`, `build.zig.zon`; also grep `Gemfile` for a `ruby` directive                                  |
| `go.mod` `go` directive     | Read `go.mod`; capture the directive line                                                                                                                                                                 |
| Inline language pins in CI  | Grep workflows for `node-version:`, `ruby-version:`, `go-version:`, `python-version:`, `zig-version:` (without `-file` suffix)                                                                            |
| `go install` pins           | Grep for `go install @` where `` is `latest`, a `vN.Y.Z` tag (with optional prerelease), or a pseudo-version (`v0.0.0-YYYYMMDDhhmmss-`)                                           |
| `cargo install` pins        | Grep for `cargo install` with or without `--locked --version`                                                                                                                                             |
| `pip` / `uv` pins           | Grep for `pip install`, `uv pip install`, `uv add`, `uv tool install`, `uvx` (with or without `==`)                                                                                                       |
| `npx` pins                  | Grep for `npx ` (with or without `@version`)                                                                                                                                                        |
| Schema URLs                 | Grep `*.json` and `*.yaml` for `$schema` URLs containing `@latest`. Pinning is per-publisher (see step 7).                                                                                                |

Exclude vendored directories from all greps: `node_modules/`, `.yarn/`, `vendor/`, `dist/`, `target/`, `.venv/`. Markdown templates with `uses:` refs that the consuming repo distributes downstream (skill scaffolds, README install snippets, etc.) are still in scope: real refs there should be pinned and refreshed against upstream just like CI workflows. The deliberate exclusions for placeholder paths (`OWNER/REPO`, ``, etc.) are documented in step 7 and apply to install commands, not `uses:` refs.

Print the output as a Markdown table grouped by category, with one row per file showing the file path, the count of matching refs, and a one-word state (`pinned` / `unpinned` / `mixed`).

### 2. Confirm Scope with the User

Present the categorized findings. The default is to pin everything in the repo that is not deliberately user-facing. User-facing means scaffolded README install instructions, placeholder paths in skill templates, and similar documentation that downstream users will customize.

Ask the user to confirm or trim the scope. Offer per-category opt-out (e.g., "skip pip pinning", "skip Dependabot config", "actions only"). If the user invokes the skill with `--scope `, use that list directly; otherwise prompt.

If the user requested `--dry-run`, or invoked the skill with audit-only phrasing (e.g. "audit version pins", "audit pins", "report unpinned versions", or any other request that asks for findings without changes), perform the audit only and stop here. Treat all audit-shaped trigger phrases the same as `--dry-run` so the README's "audit version pins" example does not silently fall into the mutating path.

### 3. SHA-Pin GitHub Actions

For every `uses:` ref to a third-party or org-owned action:

1. Resolve the ref to a 40-character commit SHA. Prefer `gh api repos//commits/` (always returns the commit SHA directly, even for annotated tags). If that fails, fall back to `gh api repos//git/ref/tags/` and recurse through the tag object until a commit SHA is found.
2. Replace the tag with the SHA and append a `# vX.Y.Z` comment. Example: `uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2`.
3. Channel-style refs (e.g. `dtolnay/rust-toolchain@stable`): pin to the SHA of the named branch's HEAD (`gh api repos//commits/`) and use `# stable` (or the branch name) as the comment.
4. Reusable workflow refs (`org/repo/.github/workflows/foo.yml@vN`): pin them too. Tag immutability is not a function of repo ownership.
5. First-party `./` refs (e.g. `uses: ./.github/actions/local`): leave unpinned; they resolve to the current commit by definition.

Reference: `./references/github-actions.md` for full recipes including annotated-vs-lightweight tag handling and how to bump a held major version.

### 4. Pin Language Runtimes via Version Files

Replace inline pins in scaffolded CI with version-file refs so the version of record is a single file in the repo:

| Inline form                       | Version-file form                                                                                                                                                                           |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node-version: "X"`               | `.tool-versions` if it has a `nodejs` line, else `.nvmrc`, else `.node-version`                                                                                                             |
| `ruby-version: "X"`               | `.tool-versions` if it has a `ruby` line, else `.ruby-version`, else `Gemfile` (if it has a `ruby` directive)                                                                               |
| `go-version: "stable"` or `"X.Y"` | `go-version-file: "go.mod"`                                                                                                                                                                 |
| `python-version: "X.Y"`           | `astral-sh/setup-uv`: omit input (reads `pyproject.toml` `requires-python` directly). `actions/setup-python`: `python-version-file: ".python-version"`; create `.python-version` if missing |
| `zig-version: "X.Y.Z"`            | Action-direct (`mlugg/setup-zig`): omit (reads `build.zig.zon`). Wrapper (`cboone/gh-actions/.../run-zig-ci.yml` v2.2.0+): `zig-version-file: "build.zig.zon"`                              |

**Reuse the file that already pins this language.** Audit step 1 already enumerates which version files exist in the repo and what each one contains. Mere existence is not enough — a `.tool-versions` that lists only `python` and `ruby` will not configure Node when CI loads it. Verify the file has an entry for the language being configured before pointing the `*-version-file` input at it. If `.nvmrc` is present, point `node-version-file` at it instead of introducing a parallel `.tool-versions`. The whole purpose of the version-file rewrite is to make CI and local dev agree on a single source of truth; emitting a second file (or pointing at a file that lacks the relevant entry) silently re-creates the drift or breaks the workflow.

If no version file pins this language, either add a line to an existing `.tool-versions` (preferred when one is already present) or create the language's conventional file (`.tool-versions` with the language's line, `.python-version` for Python, `rust-toolchain.toml` for Rust) with current LTS / stable values. Reference `./references/language-runtimes.md` for the LTS / stable lookup commands per language and the per-language fallback order.

**Python special case: do not create `.python-version` for `astral-sh/setup-uv` workflows.** uv reads `pyproject.toml`'s `[project] requires-python` directly, so `pyproject.toml` is the version source. Adding a separate `.python-version` creates a second source that uv ignores and that local tooling and CI can drift apart on; the established convention in this repo (see `plugins/set-up-ci/skills/set-up-ci/SKILL.md` and `plugins/set-up-ci/skills/set-up-ci/references/ci-python.md`) is to keep `requires-python` as the only version source for uv workflows. If `requires-python` is missing, add it to `pyproject.toml`; do not create a parallel `.python-version`. Only create `.python-version` when the workflow uses `actions/setup-python`, which has no equivalent automatic source.

**Normalize the file's value to an exact `X.Y.Z` before treating the rewrite as a pin.** Existing version files routinely carry moving forms that look pinned but aren't: `.nvmrc` accepts aliases like `lts/*`, `lts/iron`, `node`, and `latest`; `.ruby-version`, `.python-version`, and `.tool-versions` entries are often major-only (`3.4`) or truncated to major+minor (`3.13`). CI that reads any of those values resolves them at install time, so each fresh runner can pick a different patch release while the workflow still claims to be "pinned to" the file. After selecting (or creating) the file, inspect its current contents and, if the value is anything other than an exact `X.Y.Z`, rewrite it to the matching exact release **within the existing release line**: the latest patch in the same major for Node.js (so `.nvmrc = 23` becomes the highest `23.x.y`, not the current LTS major), and the latest patch in the same X.Y feature line for Ruby and Python (so `.ruby-version = 3.4` becomes the highest `3.4.z`, not whatever `3.5.x` may already be cut). Reach for the global LTS / stable lookup only when no version file existed before this pass and a fresh release line is being chosen. See `./references/language-runtimes.md` for both the in-series and the create-from-scratch lookup commands. Only then has the runtime actually been pinned.

### 5. Pin the Corepack `packageManager` Field with SHA-512 Integrity

If `package.json` has `"packageManager": "@X.Y.Z"`:

1. Compute the SHA-512 integrity hash via Corepack itself. Run `corepack use yarn@X.Y.Z` for Yarn or `corepack use pnpm@X.Y.Z` for pnpm — either form downloads the requested release, computes the integrity hash, and writes the suffixed `@X.Y.Z+sha512.` form into `package.json`'s `packageManager` field in one command. (`corepack prepare ... --activate` only prepares and activates the binary globally; it does not touch `package.json`.) Fall back to fetching the upstream tarball and computing `shasum -a 512` if Corepack is unavailable: Yarn lives at `https://repo.yarnpkg.com/${X.Y.Z}/packages/yarnpkg-cli/bin/yarn.js`; pnpm lives at `https://registry.npmjs.org/pnpm/-/pnpm-${X.Y.Z}.tgz`. Use `shasum -a 512` (portable across macOS and Linux); `sha512sum` is Linux-only.
2. Rewrite the field as `"@X.Y.Z+sha512."`.
3. Verify with `corepack enable && (yarn|pnpm) --version`.

npm-managed projects have no equivalent integrity surface in `package.json` — rely on `package-lock.json`'s per-package integrity hashes plus `npm ci` in CI. Skip this step when `packageManager` is absent or names `npm`.

Reference: `./references/yarn-corepack.md` (Yarn-specific `.yarnrc.yml` hardening covered in step 8 does not apply to pnpm).

### 6. Exact-Pin Package-Manager Dependencies (Application Context Only)

Strip `^`/`~` ranges from manifests, replacing each with the exact version locked in the lockfile. **Discriminate library from application before touching the manifest** — exact-pinning a library's manifest breaks downstream version unification.

| Ecosystem | Application discriminator                                                                                                                         | Library discriminator                                              | Default if app     |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ |
| Node.js   | Lockfile committed AND (`"private": true` OR no published `name` exposure)                                                                        | Library packages with `name`/`exports` configured for distribution | Pin manifest       |
| Ruby      | No `*.gemspec`; `Gemfile.lock` committed                                                                                                          | `*.gemspec` present (gem)                                          | Pin from lockfile  |
| Python    | `requirements.txt` or `uv.lock` committed; no library distribution name                                                                           | `pyproject.toml` declares a published distribution                 | Pin to `==`        |
| Rust      | `Cargo.lock` committed AND crate has a binary target (`[[bin]]`, `src/main.rs`, or `src/bin/*.rs`; `publish = false` is also a strong app signal) | Crate exposes `[lib]`; `Cargo.lock` typically not committed        | Exact-pin manifest |

When the discriminator is ambiguous (monorepo workspaces, hybrid crates, gems with bin entrypoints), prompt the user. **Always pin the lockfile** even for libraries — that is what `yarn install --frozen-lockfile` and friends consume in CI. Only the *manifest* changes between app and library treatment.

**Never exact-pin Node.js `peerDependencies`.** They express the range of host versions a package is compatible with; rewriting them to `==X.Y.Z` overconstrains downstream installers and can break otherwise compatible consumers. Leave the existing range (caret, pessimistic, or `>=`) intact even when pinning `dependencies` and `devDependencies`. The same logic applies to `optionalPeerDependencies`. The audit in step 1 already excludes `pe

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cboone](https://github.com/cboone)
- **Source:** [cboone/agent-harness-plugins](https://github.com/cboone/agent-harness-plugins)
- **License:** MIT
- **Homepage:** https://github.com/cboone/agent-harness-plugins

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cboone-agent-harness-plugins-pin-everything
- Seller: https://agentstack.voostack.com/s/cboone
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
