Skip to content
oss-kit
GitHubInstall

The standard

This document states every opinion oss-kit holds about an open source repository, one numbered rule at a time. A rule has an ID, a statement, the reason it exists, a Check: line naming the evidence a reader or a tool can look for, the skill that fixes it, and the forges it applies to. The skills in this repository cite these IDs instead of restating the opinions, so a rule changes in one place. oss-audit scores a repository against these IDs.

Some rules name a fallback marked below the bar. That marker means the blessed option is unavailable, not that the fallback is acceptable practice: a registry with no trusted publishing, a forge with no attestation support, a plan tier with no protected environments. Take the fallback only when the platform leaves you no other option, and revisit it when the platform catches up.

Rule IDs are R-<AREA>-<NN>. Areas are DOC, COM, CI, SEC, PUB, CHG, and SKL. IDs are permanent: a retired rule keeps its number and is marked retired rather than reused.

R-DOC-01: The README opens with one sentence saying what the project does

Section titled “R-DOC-01: The README opens with one sentence saying what the project does”

A reader decides in about five seconds whether to keep reading. A title followed by a badge wall or a table of contents spends that budget on nothing.

Check: the first paragraph of README.md after the title is a single sentence naming what the project is and who it is for, and it appears before any table of contents, badge row, or ## heading.

Fixed by: oss-readme Forges: both

R-DOC-02: The README shows how to install the project and one runnable example, in that order, near the top

Section titled “R-DOC-02: The README shows how to install the project and one runnable example, in that order, near the top”

Installation and a working snippet answer the two questions every visitor has. Burying them under motivation, philosophy, or comparison tables costs you the readers who would have used the project.

Check: README.md contains a fenced code block with an install command, followed by a second fenced code block showing minimal usage, and both appear before any section about design, motivation, or comparisons.

Fixed by: oss-readme Forges: both

Section titled “R-DOC-03: The README links to the license, the changelog, and the contributing guide”

These three files answer whether a reader may use the project, what changed since they last looked, and how to send a fix. A link that resolves to nothing is worse than no link, because it costs a click to learn the file is missing.

Check: README.md contains links whose targets are the license file, CHANGELOG.md, and CONTRIBUTING.md, and each target exists in the repository.

Fixed by: oss-readme Forges: both

R-DOC-04: Every version, command, and support claim in the README matches the repository

Section titled “R-DOC-04: Every version, command, and support claim in the README matches the repository”

Documentation drifts silently. A README promising support for a runtime version the test matrix dropped two releases ago sends a contributor into an afternoon of debugging that ends in your issue tracker.

Check: runtime versions, package versions, install commands, and CLI flags quoted in README.md appear with the same values in the package manifest, the CI configuration, and the source.

Fixed by: oss-readme Forges: both

R-DOC-05: Documentation prose is plain, active, and free of marketing language

Section titled “R-DOC-05: Documentation prose is plain, active, and free of marketing language”

Documentation is read by someone who is already stuck. Promotional adjectives and hedging add reading time without adding information, and they make the honest parts harder to trust. Write sentences in the active voice and name the actor, so a reader learns who has to do the thing. The check below names only evidence a tool can count, because an auditor that scores the same files differently on two runs makes every other score in the report unreliable.

Check: README.md, files under docs/, CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, and the entry text under CHANGELOG.md release headings contain no em dash (U+2014), en dash (U+2013), or emoji character; every heading is sentence case except a heading preserved from an attributed third-party code of conduct; and none of the words robust, powerful, seamless, comprehensive, blazing, or effortless describes the project.

Fixed by: oss-writing Forges: both

R-COM-01: The repository ships a license file whose license matches the package manifest

Section titled “R-COM-01: The repository ships a license file whose license matches the package manifest”

Without a license file the default is exclusive copyright, so nobody may legally use the code. A manifest field saying MIT while the file says Apache-2.0 forces every downstream legal review to stop and ask.

Check: LICENSE or LICENSE.md exists at the repository root, and every package manifest that declares a license names the license that file contains.

Fixed by: oss-community Forges: both

R-COM-02: CONTRIBUTING.md tells a newcomer how to set up, test, and submit a change

Section titled “R-COM-02: CONTRIBUTING.md tells a newcomer how to set up, test, and submit a change”

A contributor who cannot run the tests sends a patch you have to fix yourself. The three commands that get them from clone to green cost you one paragraph and save every future contributor an hour.

Check: CONTRIBUTING.md exists at the repository root, in .github/, or in docs/ on GitHub, or at the repository root on GitLab, and states the setup command, the test command, and how to open a pull request or merge request.

Fixed by: oss-community Forges: both

R-COM-03: CODE_OF_CONDUCT.md exists and names a working reporting contact

Section titled “R-COM-03: CODE_OF_CONDUCT.md exists and names a working reporting contact”

A code of conduct with [INSERT CONTACT METHOD] still in it is worse than none, because it advertises a reporting channel that goes nowhere.

Check: CODE_OF_CONDUCT.md exists at the repository root, in .github/, or in docs/ on GitHub, or at the repository root on GitLab, and contains an email address or a reporting URL with no template placeholder text.

Fixed by: oss-community Forges: both

R-COM-04: SECURITY.md states a private reporting channel and a response window

Section titled “R-COM-04: SECURITY.md states a private reporting channel and a response window”

Without a stated channel, a finder either opens a public issue that discloses the bug to everyone at once, or gives up. A stated response window tells them when to escalate.

Check: SECURITY.md exists at the repository root, in .github/, or in docs/ on GitHub, or at the repository root on GitLab, and names a private channel that an unaffiliated reporter can use, together with the time you commit to responding in. Accept GitHub private vulnerability reporting only when the repository is public and the feature is enabled. Accept a GitLab confidential issue only when the intended reporter role can create it as confidential; GitLab Service Desk and a monitored security email are valid alternatives.

Fixed by: oss-community Forges: both

R-COM-05: Issue and change-request templates exist so reports arrive with the facts you need

Section titled “R-COM-05: Issue and change-request templates exist so reports arrive with the facts you need”

Every free-form bug report costs a round trip to ask for the version and the reproduction. A template collects both before the issue is filed.

Check: the repository has .github/ISSUE_TEMPLATE/ with at least one template plus .github/pull_request_template.md, or .gitlab/issue_templates/ with at least one template plus .gitlab/merge_request_templates/.

Fixed by: oss-community Forges: both

R-COM-06: A CODEOWNERS file assigns a reviewer to every path

Section titled “R-COM-06: A CODEOWNERS file assigns a reviewer to every path”

Without a catch-all owner, a change to an unclaimed directory waits for someone to notice it. With one, the forge requests review automatically.

Check: where the forge supports Code Owners for the repository’s visibility and plan, a CODEOWNERS file exists in the repository root, .github/, .gitlab/, or docs/, and it contains a * rule naming at least one eligible owner. GitLab Free and private repositories on GitHub Free fall outside this rule.

Fixed by: oss-community Forges: both

R-CI-01: CI runs on every push to the default branch and on every change request

Section titled “R-CI-01: CI runs on every push to the default branch and on every change request”

A pipeline that only runs on tags tells you the build broke after you shipped it. Running on the default branch and on every pull or merge request catches breakage while the author still has the context.

Check: the CI configuration triggers on push to the default branch and on pull_request (GitHub), or defines rules for $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH and merge_request_event (GitLab).

Fixed by: oss-ci Forges: both

R-CI-02: CI runs the same lint, test, and build commands the contributing guide gives to humans

Section titled “R-CI-02: CI runs the same lint, test, and build commands the contributing guide gives to humans”

When CI runs a different command than CONTRIBUTING.md documents, a contributor passes locally and fails in CI, and neither of you can tell which is authoritative.

Check: the CI configuration invokes every applicable lint, typecheck, test, and build command defined by the project’s automation, and each command string agrees with CONTRIBUTING.md.

Fixed by: oss-ci Forges: both

R-CI-03: The test matrix covers every runtime version the project claims to support

Section titled “R-CI-03: The test matrix covers every runtime version the project claims to support”

A support claim you do not test is a guess. Dropping the oldest supported version from the matrix is how a patch release breaks half the installed base.

Check: the matrix entries in the CI configuration cover every maintained runtime release line included by the support range declared in the package manifest.

Fixed by: oss-ci Forges: both

R-CI-04: Dependency caches are keyed on the lockfile

Section titled “R-CI-04: Dependency caches are keyed on the lockfile”

A cache key that ignores the lockfile serves stale dependencies after an upgrade, so CI tests a dependency set nobody has. A key that changes on every run caches nothing and pays the restore cost anyway.

Check: every dependency cache in the CI configuration caches only reusable package-manager data, derives its primary key from the lockfile, and separates every operating system, architecture, runtime, or package-manager boundary that makes the cached contents incompatible. Any fallback key preserves those compatibility boundaries.

Fixed by: oss-ci Forges: both

R-CI-05: Every job has a timeout, and superseded runs for the same branch are cancelled

Section titled “R-CI-05: Every job has a timeout, and superseded runs for the same branch are cancelled”

A hung job holds a runner until the platform’s default timeout expires, which is six hours on GitHub. Queued runs for commits nobody will merge burn the same minutes.

Check: every job sets timeout-minutes (GitHub) or timeout (GitLab), and the configuration sets concurrency with cancel-in-progress: true for change-request runs (GitHub) or marks jobs interruptible: true with auto-cancel enabled (GitLab).

Fixed by: oss-ci Forges: both

R-SEC-01: Pin every external action and reusable workflow to a full commit SHA

Section titled “R-SEC-01: Pin every external action and reusable workflow to a full commit SHA”

Tag and branch refs are mutable, so a compromised upstream tag changes what runs in your workflow without a diff in your repo.

Check: every external uses: line in .github/workflows/, including GitHub-owned actions and reusable workflows outside the current repository, resolves to a 40-character commit SHA.

Fixed by: oss-harden Forges: github

R-SEC-02: Workflows declare least-privilege permissions

Section titled “R-SEC-02: Workflows declare least-privilege permissions”

The default token permission set is broad enough that a compromised build step can push commits or publish a release. A read-only default costs one line and turns most injection findings into nothing.

Check: every workflow file sets a top-level permissions: block that grants no more than contents: read, and any job needing more declares the extra scope at the job level.

Fixed by: oss-harden Forges: github

R-SEC-03: Automated dependency updates cover both application dependencies and CI dependencies

Section titled “R-SEC-03: Automated dependency updates cover both application dependencies and CI dependencies”

Pinning actions to SHAs (R-SEC-01) freezes them until something unfreezes them. Without an updater, pinned means unpatched.

Check: the repository contains .github/dependabot.yml or a Renovate configuration, and its ecosystem list covers both the package manager the project uses and the CI action or container images it runs.

Fixed by: oss-harden Forges: both

R-SEC-04: The default branch requires review and passing checks before merge, and rejects force pushes

Section titled “R-SEC-04: The default branch requires review and passing checks before merge, and rejects force pushes”

Branch protection is the only rule here that a repository setting enforces rather than a file. Without it, every other rule in this document can be bypassed by one push.

Check: the default branch is protected, requires at least one approving review, requires the CI status check to pass, and blocks force pushes and deletion. Read the settings with gh api repos/{owner}/{repo}/branches/{branch}/protection on GitHub, or GET /projects/:id/protected_branches/:name on GitLab.

Fixed by: oss-harden Forges: both

R-SEC-05: Release tags are signed and verifiable

Section titled “R-SEC-05: Release tags are signed and verifiable”

An unsigned tag proves nothing about who cut the release. Anyone with write access, or anyone who takes it, can point a tag at any commit. Forge verification reflects keys and identities registered with that forge and differs by signature format, so the portable evidence comes from git itself against a maintainer-controlled trust source.

Check: after git fetch --tags and importing the maintainer’s published signing key, git tag -v <tag> on the newest release tag succeeds, and git cat-file -t <tag> prints tag, which means the tag is annotated rather than lightweight.

Fixed by: oss-harden Forges: both

R-SEC-06: A GitLab pipeline pins external execution inputs and restricts inbound job-token access

Section titled “R-SEC-06: A GitLab pipeline pins external execution inputs and restricts inbound job-token access”

GitLab has no action-shaped uses: step, so the same mutable-reference problem arrives through image:, services:, and external include: entries. A floating image tag or an include: on a branch name changes what runs without a diff in your repository. Separately, an open inbound job-token scope lets a job token from any project access this project when its triggering user is authorized. The allowlist is a target-project control; it does not describe where this project’s own job token can go.

Check: every image: and services: entry in .gitlab-ci.yml and its included files names an image by digest (image@sha256:...), every external include:project and include:component entry resolves to a full commit SHA, every include:remote entry sets integrity:, and GET /projects/:id/job_token_scope reports inbound_enabled true with its allowlist naming only source projects that need access to this target project.

Fixed by: oss-harden Forges: gitlab

R-SEC-07: Untrusted input never reaches a privileged context

Section titled “R-SEC-07: Untrusted input never reaches a privileged context”

Forge-supplied text such as an issue title, a branch name, or a commit message is written by whoever opened the contribution. Interpolated into a shell command it runs as code, and a workflow that builds a fork’s contribution while holding secrets hands those secrets to its author.

Check: on GitHub, no user-controlled ${{ github.event.* }} or ${{ github.head_ref }} expression appears inside a run: block, such values reach the shell through an env: block instead, and no pull_request_target or workflow_run workflow executes contributor-controlled code; on GitLab, user-controlled predefined variables are quoted when used in script:, no job evaluates them as generated shell, and every sensitive variable is masked with variable-reference expansion disabled.

Fixed by: oss-harden Forges: both

R-SEC-08: Registry dependencies resolve through a committed lockfile

Section titled “R-SEC-08: Registry dependencies resolve through a committed lockfile”

A version range re-resolves on every build, so what CI installs today is not what it installed yesterday. A lockfile records the exact artifact and its hash, which turns a silent substitution into a failed build. It costs a maintainer nothing, because the package manager writes it.

Check: the lockfile or fully hashed requirements file the project’s package manager produces is committed, records integrity for registry dependencies, and every CI install uses the package manager’s current frozen mode so it fails on stale or missing resolution data rather than updating it.

Fixed by: oss-harden Forges: both

R-SEC-09: Static analysis runs on pull requests where the language supports it

Section titled “R-SEC-09: Static analysis runs on pull requests where the language supports it”

A reviewer catches what a reader notices. A static analyzer catches the injection and memory classes a reader skims past, and on GitHub the default setup is a repository setting rather than a workflow to maintain.

Check: where the repository contains source in a language a static analyzer supports, a static analysis workflow runs on pull requests to the default branch and its result is a required check. A repository holding no source in a supported language falls outside this rule rather than failing it.

Fixed by: oss-harden Forges: both

R-PUB-01: Publishing happens in CI, triggered by a tag, never from a developer machine

Section titled “R-PUB-01: Publishing happens in CI, triggered by a tag, never from a developer machine”

A local publish ships whatever is in the working tree, from a machine holding a long-lived registry token. A tag-triggered CI publish ships a commit that is in the repository and that CI has tested.

Check: a release workflow or pipeline triggered by a tag push runs the publish command, and the publish command appears in no local script intended for manual use.

Fixed by: oss-publish Forges: both

R-PUB-02: The publish job authenticates to the registry with trusted publishing, not a stored token

Section titled “R-PUB-02: The publish job authenticates to the registry with trusted publishing, not a stored token”

A long-lived registry token in CI secrets is the single credential that turns any workflow compromise into a supply-chain compromise. Trusted publishing exchanges a short-lived OIDC token per run, so there is nothing to steal between releases.

Check: the publish job requests id-token: write and publishes through the registry’s OIDC flow (npm trusted publishing, PyPI trusted publishers, RubyGems OIDC, crates.io trusted publishing). Where the registry offers no OIDC flow, a scoped token limited to one package is below the bar and permitted.

Fixed by: oss-publish Forges: both

R-PUB-03: Published artifacts carry build provenance

Section titled “R-PUB-03: Published artifacts carry build provenance”

Provenance links the published artifact back to the commit and workflow that built it, so a consumer can tell a legitimate release from one uploaded by whoever held the token.

Check: the exact published artifact has verifiable provenance tied to the expected repository and workflow. Prefer a registry-served record: verify npm provenance for an installed exact package version, PyPI provenance through its Integrity API, or a RubyGems attestation through its attestation API. Where a registry cannot serve provenance, verify a forge attestation for the exact published artifact and report the registry limitation; OIDC authentication alone is not provenance.

Fixed by: oss-publish Forges: both

R-PUB-04: A human approves the run before anything reaches a public registry

Section titled “R-PUB-04: A human approves the run before anything reaches a public registry”

A registry publish cannot be undone. An approval gate is the last point where a compromised tag, a wrong version, or a bad artifact can be stopped.

Check: before public availability, a person other than an automation account must approve through a GitHub environment with required reviewers, a GitLab protected environment with a manual job and approval rules, or a registry proof-of-presence gate such as npm staged publishing with 2FA approval. Verify the configured gate through the forge or registry API. If the repository visibility or forge plan does not provide a native gate and the registry has no equivalent, report the rule as unmet rather than substituting an unverified approval action.

Fixed by: oss-publish Forges: both

R-CHG-01: Each release unit keeps a discoverable changelog in its declared format

Section titled “R-CHG-01: Each release unit keeps a discoverable changelog in its declared format”

A changelog exists so a user upgrading two versions can read what changed without diffing tags. Generated commit lists do not answer that, because commit subjects address the maintainer, not the user.

Check: each release unit has a discoverable changelog following the project’s declared convention. For Keep a Changelog 2.0.0, it opens with # Changelog and a pinned convention link, carries ## [Unreleased], lists dated releases newest first, groups notable changes under the six standard types, marks incompatible entries **Breaking:**, and resolves version headings to tag or comparison links.

Fixed by: oss-changelog Forges: both

R-CHG-02: Semantic versions reflect changes to the declared public API

Section titled “R-CHG-02: Semantic versions reflect changes to the declared public API”

SemVer is a promise to users and dependency resolvers about the declared public API. A version increment that understates an incompatible change lets an ordinary upgrade select code the version range did not warn users about.

Check: the project declares the public API covered by Semantic Versioning; versions use valid SemVer syntax, allowing a v prefix only on tag names; stable releases increment MAJOR for incompatible public API changes, MINOR for compatible additions and deprecations, and PATCH for compatible fixes. During 0.y.z initial development, this standard uses MINOR for incompatible changes rather than forcing 1.0.0; SemVer itself permits anything to change before 1.0.0.

Fixed by: oss-changelog Forges: both

R-CHG-03: Every release unit uses one version across its tag, manifests, and changelog

Section titled “R-CHG-03: Every release unit uses one version across its tag, manifests, and changelog”

When these disagree, nobody can tell which one describes the artifact users installed, and the changelog stops being a reliable upgrade record. A repository that ships several manifests, one per host or one per package, multiplies the ways they can drift apart.

Check: for the newest release of each release unit, its tag, every manifest or generated version source that describes that unit, and its newest changelog entry name the same version. Independently versioned packages in one repository are checked separately and are not forced to share a version.

Fixed by: oss-changelog Forges: both

R-CHG-04: Forge release notes derive from the changelog without contradicting it

Section titled “R-CHG-04: Forge release notes derive from the changelog without contradicting it”

Auto-generated release notes list merged pull requests, which repeats work the changelog already did better. Two divergent descriptions of one release is worse than one.

Check: the release body on the forge contains the corresponding changelog section without omissions or contradictory claims. It may add release-scoped installation, verification, migration, asset, or contributor details. Read it with gh release view <tag> --json body on GitHub, or from the description field of GET /projects/:id/releases/:tag_name on GitLab.

Fixed by: oss-changelog Forges: both

R-CHG-05: A public API is deprecated in a release before it is removed

Section titled “R-CHG-05: A public API is deprecated in a release before it is removed”

Removing an API without warning turns an upgrade into an outage. Users need a released version where the old path still works and the interface they use directs them to the replacement.

Check: every public item under Removed appeared under Deprecated in an earlier release, stayed usable for the project’s stated deprecation window, and produced an interface-appropriate notice naming the replacement or migration path and earliest removal version. For stable SemVer, deprecation ships in a MINOR release and removal waits for a later MAJOR release.

Fixed by: oss-changelog Forges: both

This area applies only to a repository that ships agent skills, meaning a repository holding at least one SKILL.md. Where none exists, the area is not applicable as a whole and its rules are not checked one at a time.

R-SKL-01: Skills live in a top-level skills/ directory, one directory per skill

Section titled “R-SKL-01: Skills live in a top-level skills/ directory, one directory per skill”

The skills CLI discovers this layout directly, and plugin packages conventionally expose the same directory. Keeping one canonical tree avoids copies drifting while host-specific paths can point to it through manifests or symlinks.

Check: a skills/ directory exists at the repository root, every skill is a direct child of it and holds a SKILL.md, and no other SKILL.md exists in the repository. Resolve a symlinked directory such as .claude/skills to its target before applying the check, so a committed symlink pointing at skills/ does not read as a second copy.

Fixed by: oss-skill Forges: both

R-SKL-02: Every skill conforms to the Agent Skills specification

Section titled “R-SKL-02: Every skill conforms to the Agent Skills specification”

The specification is the one format every host reads. A skill that violates it fails to load, and most hosts fail silently, so the author sees a skill that never triggers and no error saying why.

Check: a specification validator exits 0 for every directory under skills/. oss-skill ships one at scripts/validate.mjs in its own installed directory, which reads files, needs nothing installed, and runs on Node 22 or later and on Bun.

Fixed by: oss-skill Forges: both

R-SKL-03: A SKILL.md body stays under 500 lines, with depth in references/

Section titled “R-SKL-03: A SKILL.md body stays under 500 lines, with depth in references/”

The whole body loads into context when the skill activates, competing there with the conversation and with every other active skill. The specification recommends fewer than 500 lines and 5000 tokens, with supporting files loaded only when the task calls for them.

Check: every skills/*/SKILL.md is under 500 lines, and any skill needing more material ships it under that skill’s own references/ directory.

Fixed by: oss-skill Forges: both

R-SKL-04: Every SKILL.md declares a license

Section titled “R-SKL-04: Every SKILL.md declares a license”

Installers extract one skill directory at a time. The repository license file does not travel with it, so an extracted skill arrives with no terms attached and nobody downstream can tell whether they may use it.

Check: the frontmatter of every skills/*/SKILL.md carries a license: field naming the license that applies to that skill. It matches the repository license when that license covers the skill; a differently licensed skill carries its own license file and the field references it.

Fixed by: oss-skill Forges: both

R-SKL-05: A skill that ships a script uses portable sh or JavaScript with no dependencies

Section titled “R-SKL-05: A skill that ships a script uses portable sh or JavaScript with no dependencies”

A script inside a skill runs on the reader’s machine, not the author’s. An interpreter the reader does not have, or a dependency install the skill cannot perform, turns a skill that loads into a skill that fails partway through the task it was invoked for.

Check: within a skill’s scripts/ directory, at any depth, every executable file starts with a shebang naming sh or node, directly or through env; JavaScript resolves every import and require to a relative path, an absolute path, or a real Node built-in module; no script uses TypeScript, a runtime-specific global, or a runtime-specific module; and the skill directory contains no dependency manifest, lockfile, or node_modules. A skill that ships no script and no manifest falls outside this rule rather than failing it.

Fixed by: oss-skill Forges: both

R-SKL-06: Every host the repository claims support for has a working install path

Section titled “R-SKL-06: Every host the repository claims support for has a working install path”

A README that names a host it has shipped nothing for sends a reader to a command that fails. Hosts read different manifests, so support is a file at a path, not a sentence.

Check: for every host named in README.md or in the install documentation it links to, either the manifest that host reads is committed at the path the host reads it from, or a documented install command exists that needs no manifest; and no host is named that has neither.

Fixed by: oss-skill Forges: both

R-SKL-07: Each skill is a focused, progressively disclosed procedure

Section titled “R-SKL-07: Each skill is a focused, progressively disclosed procedure”

A skill adds value only when it triggers for the right task, supplies knowledge the agent lacks, and leads the work through a repeatable procedure. Generic explanation, broad menus of equal choices, and unnecessary reference loads consume context without improving the result.

Check: the description states both the task outcome and when to use the skill; the body keeps one coherent workflow, gives a default when several approaches exist, preserves non-obvious gotchas, and includes a verification loop; conditional detail lives in directly linked files loaded only at the branch that needs it; and the instructions name capabilities rather than harness-specific tools.

Fixed by: oss-skill Forges: both