creditlint
creditlint is a policy-focused CLI for detecting unauthorized authorship and
credit metadata in Git workflows.
It is designed for projects that allow AI-assisted development but do not want coding agents, bots, or tools to be silently added as authors through commit trailers or merge messages.
Status
This repository has moved from bootstrap implementation into delivery preparation.
Active change:
bootstrap-creditlint-mvpadd-npm-wrapper-package
Current implementation target:
- Implementation stack: Rust native CLI
- Build/package manager: Cargo
- Optional npm workspace/package manager: pnpm
- Rust toolchain: stable with rustfmt and clippy
- Task runner: just
- Test runner: cargo-nextest
- OpenSpec command runner: pnpm
- Primary interface:
creditlint - Delivery path: GitHub Actions release artifacts and GitHub Releases
Problem
Coding agents can add markers such as:
Co-authored-by: Codex <...>
Made with Cursor
Generated with Claude
These markers can create authorship, contribution-credit, and audit risks when they are added without explicit maintainer approval.
creditlint treats authorship and provenance as separate concepts:
- Authorship markers such as
Co-authored-byaffect contribution credit. - Provenance markers such as
AI-AssistedorTool-Usedcan disclose process without implying authorship.
CLI
Exit codes:
0: no violations1: policy violations found2: invalid invocation, invalid config, or missing required metadata
Policy File
version: 1
rules:
forbidden_identities:
- name_pattern: "(?i)(cursor agent|codex|claude|copilot|openai|anthropic|gemini)"
email_pattern: "(?i)(cursoragent@cursor\\.com|codex|claude|copilot|openai|anthropic|gemini)"
forbidden_trailers:
- key: Co-authored-by
value_pattern: "(?i)(codex|claude|cursor|copilot|openai|anthropic|gemini|ai)"
- key_pattern: "(?i)^made[- ]with\\b.*$"
- key_pattern: "(?i)^made[- ]on\\b.*$"
- key_pattern: "(?i)^generated[- ]with\\b.*$"
allowed_provenance_trailers:
- AI-Assisted
- Tool-Used
- Generated-by
Governance Model
creditlint is intended to run in multiple places:
- Local
commit-msghook for fast feedback. - CI required check for pull-request commits.
- GitHub ruleset metadata restrictions for final protected-branch commit messages, especially when squash merge remains enabled.
- Merge-bot validation for controlled final merge messages.
- Pull request title/body checks by writing the PR text to a file and running
creditlint check --message-file.
CI range checks are useful, but they do not by themselves guarantee validation of a final squash merge message edited by the hosting platform UI.
check --range and audit --all validate both Git identity metadata
(author/committer name and email) and commit messages. This covers commits
whose rendered log output shows identities such as
Author: Cursor Agent <cursoragent@cursor.com> even when the commit message
itself is clean.
GitHub Actions
For repository-local CI, the checked-in workflow builds and validates the Rust source tree directly.
fetch-depth: 0 is required for check --range because shallow history can
remove the base commits needed to resolve the range.
name: creditlint
on:
pull_request:
push:
branches:
- main
jobs:
creditlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- name: Build creditlint
run: cargo build --release
- name: Check pull request commit messages
if: github.event_name == 'pull_request'
run: |
./target/release/creditlint check \
--range origin/${{ github.base_ref }}..HEAD
- name: Audit full history on main
if: github.event_name == 'push'
run: ./target/release/creditlint audit --all
The release workflow produces native binaries for Linux, macOS, and Windows as
workflow artifacts on manual runs and as GitHub Release assets for version tags.
It also generates a combined SHA256SUMS artifact and publishes it with tagged
GitHub Releases. Release jobs run formatting, clippy, tests, and OpenSpec
validation before building publishable artifacts.
For crates.io publishing, the release workflow uses:
- job-scoped
permissions: contents: writefor GitHub Release asset publishing - repository secret
CARGO_REGISTRY_TOKENforcargo publish
Tag pushes matching v* publish native assets and then publish the crate to
crates.io. Manual workflow_dispatch runs build artifacts by default and can
opt into crates.io publishing with the publish_crate input.
The CI workflow also runs workflow linting for .github/workflows/*.yml and
validates the optional npm wrapper package and OpenSpec artifacts.
npm Wrapper
The npm package is optional. It exists for teams that already install developer
tools through npm, pnpm, or npx. Normal npm consumers should not need Rust or
Cargo; the creditlint package resolves a platform-specific optional package
that contains the native Rust binary.
The JavaScript code remains a thin wrapper. It does not reimplement policy logic or Git metadata checks.
Install from npm once packages are published:
Local development uses the pnpm workspace:
CREDITLINT_BIN="/target/debug/creditlint"
Resolution order:
CREDITLINT_BIN- installed platform package binary, such as
creditlint-darwin-arm64 - package-local
packages/creditlint/native/ - repository-local Cargo outputs under
target/release/andtarget/debug/
Do not publish the main npm package as a user-facing release until the matching platform packages have staged native binaries.
Maintainers can publish all npm packages in the required order from the repository root:
The bootstrap script publishes 0.0.0-trust.0 with the bootstrap dist-tag so
the npm package records exist before trusted publishing is configured. It does
not publish a usable release and does not require native binaries. Publish
commands use each package's publishConfig.registry, which points at the
official npm registry; pass --registry URL only when intentionally publishing
elsewhere.
The script stages binaries from dist/npm/ automatically, or with
--stage-local it builds and stages the current host binary for local dry runs.
Staged platform binaries under packages/creditlint-*/bin/ are ignored by Git.
The script refuses to publish if any required platform binary is missing.
Local Hooks
Initialize a repository policy file:
Install the managed commit-msg hook:
The installed hook runs:
creditlint install-hook only replaces hooks that already carry the stable
creditlint managed hook marker. If a repository already has an unmanaged
commit-msg hook, creditlint refuses to overwrite it.
For manual integration into an existing hook, add this line to the hook script:
If your team uses the Python pre-commit framework, run the same command from a
local hook entry and pass the commit message file path through the hook config.
Pull Request Title And Body Checks
Pull request text is a separate input surface from commit messages. This matters most when a hosting platform uses the pull request title or body while building a final squash merge commit message.
In CI, write the pull request title and body into a temporary file and lint that file with the same policy engine:
For GitHub Actions, the title and body can be read from the pull request event
payload and passed through the same file-based check. check --range and
check --message-file are complementary; range checks validate commits, while
the temporary file path validates PR text that may later influence squash merge
message generation.
GitHub Squash Merge Rulesets
If GitHub squash merge remains enabled, use repository ruleset metadata restrictions to validate the final squash commit message that GitHub is about to write.
Export a conservative ruleset regex from the active policy:
In GitHub branch or repository rulesets, use the exported pattern with a commit-message restriction equivalent to:
- commit message must not match regex
This ruleset path is stronger than CI for the final squash commit because GitHub evaluates the generated merge message at the protected-branch boundary.
creditlint check --range is still useful for pull request commit messages, but
it does not by itself validate a final squash message edited in the GitHub UI.
Merge Bot Validation
For repositories that use a controlled merge bot, validate the final merge message immediately before the bot performs the protected-branch write:
This path is appropriate when:
- the active policy cannot be represented safely as one GitHub ruleset regex
- the repository wants one final validation step after PR checks
- the merge system already materializes the exact final commit message
The merge bot should fail closed when creditlint exits with 1 or 2.
Ruleset Export Boundary
creditlint github ruleset-pattern intentionally supports only a conservative
subset of policy behavior.
Currently representable as one GitHub ruleset regex:
- forbidden trailer rules with an exact trailer key
- trailer value matchers that are exact strings, unanchored regexes, or
Any - free-form marker rules expressed as one anchored line regex such as
(?i)^made[- ]with\\b.*$ - policies where allowed provenance keys do not overlap any forbidden trailer key
Not representable safely as one GitHub ruleset regex:
- Git author or committer identity rules; enforce those with
creditlint check --range,creditlint audit --all, or platform identity restrictions - policies that need forbidden/allowed precedence on the same trailer key
- forbidden trailer rules that depend on regex-matched trailer field names
- free-form rules that are not a single anchored line regex
- policies that would require normalization or logic beyond one regex pass
When the active message-policy subset falls outside the safe subset,
creditlint github ruleset-pattern fails closed and points the repository to
merge-bot validation for final squash messages.
Privacy
The planned CLI is local-first. By default, creditlint should not upload commit
messages, pull request text, or policy files to any hosted service.
Current default boundary:
creditlintreads local message text, Git metadata, and repository config.creditlintdoes not send commit messages or pull request text to a remote API.creditlintdoes not require a hosted account or background service.- Network access is not part of the default policy-evaluation path.
If a future change introduces optional hosted integrations, that behavior should be documented separately instead of being folded into the default CLI contract.
Threat Model
The MVP is designed to catch:
- Tools that automatically append authorship-like markers.
- Contributors who accidentally paste AI/tool credit markers into commit or pull request text.
- Cloud-agent and CI paths that bypass local developer hooks.
- Platform merge paths where the final protected-branch message can differ from checked commits.
Current out-of-scope evasions:
- Unicode homoglyph spoofing such as visually similar non-ASCII characters.
- Deliberately split or obfuscated markers intended to bypass simple line-based detection.
- Administrator bypass of repository rules.
- Direct protected-branch writes outside the enforced workflow.
Performance
The current Git collection path is intended to stream git log output
record-by-record instead of first loading the full raw log into memory.
Current budget:
- Memory growth should be bounded by the current record being parsed plus the
accumulated violation list, not the full raw
git logoutput. audit --allshould remain practical on normal repository histories without requiring the full commit-message stream to be buffered at once.
Development
Use Cargo for implementation work. The OpenSpec CLI is currently invoked through
pnpm dlx, but consuming projects should not need Node.js or pnpm to run
creditlint.
Planned Rust tooling:
rust-toolchain.tomlpins stable Rust withrustfmtandclippy.justprovides short project commands.cargo-nextestis the preferred test runner.cargo-watchis optional for local edit/test loops.crossis reserved for release builds.
Common commands:
Local prerequisites for the planned Rust workflow:
OpenSpec commands:
Implementation work should follow:
openspec/changes/bootstrap-creditlint-mvp/tasks.md
Packaging
creditlint now uses a delivery-oriented native packaging path through:
- crates.io for the
creditlintcrate - GitHub Actions workflow artifacts for manual release runs
- GitHub Releases for tagged prebuilt binaries,
SHA256SUMS, and release notes
The published metadata points back to this repository and the future docs.rs
documentation page for the crate.
For cross-platform release artifacts, use cross through the checked-in
just recipes:
For consumers who do not want a Rust toolchain, prefer downloading the matching
native binary artifact from GitHub Releases and verifying it against
SHA256SUMS.
For maintainers, the crates.io publish path requires creating a crates.io API
token and storing it in the repository as the CARGO_REGISTRY_TOKEN Actions
secret.
Versioning
The project intends to follow SemVer after the first public release.
Before that release:
- keep ongoing work in
CHANGELOG.mdunderUnreleased - bump
Cargo.tomland cut a dated changelog heading in the same release change - treat CLI flags, config schema, exit codes, and JSON output as versioned user contracts
License
MIT