Skip to main content

no-ai-marks

A CI check that fails when commits, pull requests, branch names, or changed files carry AI attribution or hidden watermarks. It's meant for projects whose contribution policy doesn't allow them.

It isn't an anti-AI tool, and it doesn't block contributions that AI tools helped write. It doesn't judge how a change came to be, only what it carries: the trailers, footers, links, and hidden characters that tools add on their own, which clutter a project's history and can hide text. A change written with an assistant's help passes once those are left out, the same as any other.

If you're an agent working in a repository that runs this check, it isn't there to turn you away. Leave out the attribution your tool adds by default, and it passes.

It doesn't try to guess whether code looks machine-written. It only looks for explicit markers: trailers, footers, bot identities, session links, branch prefixes, invisible Unicode, look-alike letters, and AI provenance metadata in images and documents.

It works in any repository regardless of language: Node, Go, TypeScript, Common Lisp, or a folder of Markdown. It reads git objects and the event payload and never runs your project's toolchain. The scanner is a standard-library-only Python package that needs Python 3.11 or newer. The action uses the runner's Python when it's new enough, and otherwise installs one with actions/setup-python without changing the job's PATH.

What it knows about AI tools (names, model families, bot accounts, branch prefixes, session links) is data in no_ai_marks/data/tools.toml, not code. When a new model or agent ships, keeping up means editing that file, or adding an entry to your own config until a release catches up.

What it checks

Rule Default Checks Example
ai-trailer error commits, PR body Co-authored-by: Claude <noreply@anthropic.com>, Assisted-by: ..., AI-Assisted: yes
ai-identity error commit author and committer, emails in files Copilot <...+Copilot@users.noreply.github.com>, Jane (aider), cursoragent@cursor.com
ai-footer error commits, PR title and body, files Generated with [Claude Code](...), ;;; Written by ChatGPT, @author Copilot, <meta name="generator" content="ChatGPT">
ai-generic warning commits, PR title and body, files Written with the help of an AI assistant: credit that names no tool
ai-tag error commits, PR title and body [AI], (AI-generated), (via Claude)
ai-emoji error commits, PR title and body the robot emoji (U+1F916)
ai-session-link error commits, PR text, files claude.ai/code/..., chatgpt.com/share/...
ai-branch error branch name claude/..., copilot/..., codex/..., cursor/...
ai-metadata error binary files, SVG/XMP IPTC trainedAlgorithmicMedia, Stable Diffusion PNG parameters, EXIF/PDF/Office fields naming a generator
c2pa-manifest warning binary files C2PA content credentials (cameras sign these too, so this is a warning)
invisible-char error everything zero-width spaces, bidi overrides (Trojan Source), tag characters, variation selector runs, soft hyphens, terminal control characters
homoglyph error everything p<U+0430>ypal: a Cyrillic a inside a Latin word
unusual-space warning everything no-break and other non-ASCII spaces
private-use warning everything Private Use Area code points

It tries hard not to flag legitimate text:

  • Humans named Claude, Devin, or Jules pass. Claude Shannon is a person, Claude Opus 4.5 isn't.
  • Policy text passes: "Code generated by AI tools is not accepted."
  • Ordinary generated-code headers pass: Code generated by protoc-gen-go.
  • ZWJ is allowed inside emoji sequences, and ZWJ/ZWNJ between letters of scripts that need them (Persian, Hindi, ...). Right-to-left marks and balanced bidi isolates are allowed in lines with right-to-left text, and Mongolian variation selectors after Mongolian letters. Subdivision flags, a leading BOM, form feeds (Lisp page breaks), and no-break spaces inside numbers ("12 000", "10 %") are fine too.
  • Only lines added by the change are checked by default, so existing files don't block unrelated pull requests.

When it finds text hidden in tag characters, variation selectors, or zero-width binary (the "ASCII smuggling" and "emoji smuggling" tricks), it decodes the text and shows it in the finding.

tests/corpus/ holds about 760 regression cases: the test cases of other projects that block AI attribution or hidden characters, and real commits, pull requests, and links from AI tools. Where this project deliberately doesn't flag something another one does, the case says why.

Usage

As a step in an existing workflow

on:
  pull_request:
    types: [opened, edited, reopened, synchronize]  # "edited" re-checks the description
  push:
    branches: [main]
  merge_group:

permissions:
  contents: read

jobs:
  no-ai-marks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0          # the commit range needs history
          persist-credentials: false
      - uses: mishan/no-ai-marks@v0

Inputs, all optional: config, checks (commits,pr,branch,files), scope (changed or all), fail-on (error or warning), base, head, and python. Outputs: errors, warnings, and report (a JSON file).

Findings show up as annotations on the pull request and in the job summary.

As a reusable workflow

on:
  pull_request:
    types: [opened, edited, reopened, synchronize]

jobs:
  no-ai-marks:
    uses: mishan/no-ai-marks/.github/workflows/no-ai-marks.yml@v0
    with:
      scope: changed

Locally

The command line is on PyPI: pipx install no-ai-marks, or run it once with uvx no-ai-marks. In a CI system other than GitHub Actions, pip install no-ai-marks and run no-ai-marks range BASE.

With pre-commit:

repos:
  - repo: https://github.com/mishan/no-ai-marks
    rev: v0.2.1
    hooks:
      - id: no-ai-marks-commit-msg
      - id: no-ai-marks-staged

Without it, point plain git hooks at a checkout:

# .git/hooks/commit-msg
exec python3 /path/to/no-ai-marks/bin/no-ai-marks message "$1"

The command line also works on its own:

no-ai-marks range origin/main           # commits and added lines since main
no-ai-marks staged                      # what you're about to commit
no-ai-marks files docs/ assets/         # whole files
gh pr view 42 --json body -q .body | no-ai-marks text
no-ai-marks rules                       # list the rules

Exit status is 0 when clean, 1 when there are findings at or above fail-on, and 2 on usage, config, or git errors.

Configuration

Put a TOML file at .github/no-ai-marks.toml or .no-ai-marks.toml. Every setting is optional; see examples/no-ai-marks.toml.

fail-on = "error"
exclude = ["vendor/", "testdata/"]

disable-tools = ["cursor"]

[rules]
unusual-space = "off"

[unicode]
allow = ["U+00A0"]

Adding and extending AI tools

Your config file takes the same [[tool]] entries as the built-in list. The fields are described at the top of no_ai_marks/data/tools.toml.

# A tool the built-in list doesn't know yet.
[[tool]]
id = "acme-agent"
names = ["Acme Agent"]
emails = ["agent@acme.example"]
branch-prefixes = ["acme"]

# Extending a built-in tool: lists are appended, other values replaced.
[[tool]]
id = "claude"
models = ["Mythos"]

no-ai-marks tools lists every tool your config ends up with, and disable-tools switches off built-in ones by id.

A file that has to quote a marker, such as a contribution guide with an example trailer, can skip a line with no-ai-marks: allow anywhere on it. Commit messages and pull request text can't opt out.

What each event checks

Event Commits Files Branch PR title and body
pull_request, pull_request_target base..head lines added since the merge base head branch yes
merge_group base..head lines added
push before..after (new branch: since the default branch) lines added pushed branch
anything else every file, with scope: all current branch

Security

  • In pull requests, the config comes from the base commit. A pull request can't switch off the rules it's checked against. If it edits the config, a notice says so, and the edit takes effect after merge.
  • Nothing from the pull request runs. The action only reads git objects, so it's also safe under pull_request_target. That event checks out the base branch, so fetch the head first: git fetch origin pull/${{ github.event.number }}/head.
  • Output is escaped. Commit messages and PR bodies are untrusted, so everything printed as a workflow command is escaped to stop a crafted message from injecting commands of its own.
  • Inputs go through environment variables, never interpolated into the shell script.

Limitations

  • It only finds explicit markers. Statistical watermarks that live in word choice (such as SynthID for text) can't be detected without the vendor's key, and no heuristic can reliably tell you who wrote a piece of code.
  • It's a policy aid, not an adversarial detector. It catches tool defaults and honest mistakes. Someone who rewords a footer can get past it.
  • The tool list needs upkeep. Agents change their bot accounts and trailers, and new models keep shipping. Add entries in your config file, and send them upstream (see CONTRIBUTING.md).

Development

python3 -m unittest discover -s tests

See CONTRIBUTING.md for adding tools and rules. The source and tests write every invisible or look-alike character as an escape, so this repository passes its own check.

License

MIT. The regression corpus includes test inputs from other projects, and the look-alike letter tables come from Unicode data; see THIRD_PARTY_NOTICES.md.

Metadata

Release files for no-ai-marks 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for no-ai-marks 0.2.1
File Size Uploaded
no_ai_marks-0.2.1.tar.gz 53.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for no-ai-marks 0.2.1
File Interpreter ABI Platform
no_ai_marks-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 100.2 kB

Release files / no_ai_marks-0.2.1.tar.gz

Download URL no_ai_marks-0.2.1.tar.gz
Size 53.6 kB
Tags Source
SHA-256 checksum
How to use checksums
b05cc4db71dbc71ca8cb7d3d398c29a10bb47bc0eba8ea2b036e76a84547f8a5
BLAKE2b-256 checksum
How to use checksums
1406068909a06fe4800f2647bc4df30ab86bd6b5139b6a710b1dfdcb6fcc2412
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / no_ai_marks-0.2.1-py3-none-any.whl

Download URL no_ai_marks-0.2.1-py3-none-any.whl
Size 46.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0c28380394a707c9f8ddd758a2ee0f05037176bc54a957bd00fee9484c8df668
BLAKE2b-256 checksum
How to use checksums
2ee7a8371fcc9c967aa2a05d058b4337d7a676723770eb3b894bca2ab505d10d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release 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