Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
# The gate refuses a tracked agent configuration; this repo is not exempt.
.claude/
56 changes: 52 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,13 +16,19 @@ on GitHub, against commits that already exist. There is nothing to skip.
| **Identity** | **every commit reachable from the branch** (default) | a commit's author or committer is not one of the `authors` accounts. Opt-in: an empty list disables it |
| **AI attribution** | **every commit reachable from the branch** (default) | any commit's message, author or committer carries an attribution: `Co-authored-by:` naming an assistant, a "Generated by/with …" banner, the 🤖 footer, an `noreply@anthropic.com` identity, "AI-assisted", a `Plan:` footer |
| **Format** | the commits this push/PR introduces (merges skipped) | a subject is not `type(scope)!: subject` with a conventional type |
| **Secrets** | lines this push/PR *adds*, outside test/fixture paths | a line looks like a credential (AWS key, GitHub token, private-key header, `password = "…"`, …) |
| **Secrets** | lines this push/PR *adds*, outside test/fixture paths | a line looks like a credential (AWS key, GitHub token, private-key header, a quoted `password` assignment, …) |
| **Agent artefacts** | **every path tracked in the tree at the head** | an AI agent's tooling configuration is committed: `.claude/`, `.cursor/`, `.cursorrules`, `.windsurf/`, `.codex/`, `.gemini/`, `.aider.*`, `.roo/`, `.cline/`, `.goose/`, `.amazonq/`, `.kiro/`, `.specstory/`, `.mcp.json`, … |

Default attribution patterns match the **shape of an attribution**, not the
mention of a vendor. Measured on `devcontainer-template`'s real history, a
bare `.claude/` rule matches 25 of the last 60 legitimate commits. Keyword
rules exist but are opt-in (`strict: true`).

That caution is about **prose**, and the artefact check is the other side of
it. A commit message naming `.claude/` is ordinary work; the string
`.claude/settings.json` in the tree *is* the thing. Same characters, no
ambiguity to be fair to — so paths are a default while keywords are not.

**Deliberately not here: lint, build, test.** Every repository's own CI
already runs those on GitHub, so `--no-verify` never bypassed them.

Expand DownExpand Up@@ -64,6 +70,44 @@ The gate reports and never rewrites; see [Rewriting history](#rewriting-history)

Set `history: range` to only check the commits a push/PR introduces.

### Agent artefacts: the tree, and a cheap way out

The artefact check reads the **tree at the head**, not the range. That is the
whole point of it: the `.claude/` that prompted the rule was merged into a
trunk long before the rule existed, and a check scoped to what a change *adds*
would call every later pull request clean while the directory sat there. The
gate stays red until the files are gone.

It can afford that scope because gone is cheap here, and this is the one place
the gate refuses something without demanding a rewrite:

```sh
git rm -r --cached .claude && echo '.claude/' >> .gitignore
git commit -m "chore: untrack the agent configuration"
```

The files stay on your machine; the repository stops carrying them. Nothing
walks the ancestry, so that single commit ends it — unlike an attribution,
which lives in a commit and needs `scripts/rewrite-history.sh`.

**Not matched, by decision rather than by omission.** Editor configuration —
`.vscode/`, `.idea/`, `.zed/` — is untouched: an editor is not an agent, and a
repository's editor settings long predate all of this. Neither is
`.devcontainer/` itself, which humans and CI read too. Neither are markdown
instructions (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`): 21 of the fleet's 55
repositories carry one, and whether that prose belongs in a repository is an
editorial call a gate is the wrong place to make.

Read that claim precisely: nothing is matched *because* it sits under
`.devcontainer/`. An agent directory nested there is still an agent directory —
the fleet's own template ships one at `.devcontainer/images/.claude/` — and
exempting a parent would make "put it under `.devcontainer/`" the way around
every rule in the file.

A repository whose purpose *is* to distribute this configuration exempts the
exact paths it ships with `agent_files_allow`, rather than by weakening the
list everyone else runs.

## Install

`.github/workflows/post-commit.yml` in the target repo (`stub/post-commit.yml` here):
Expand DownExpand Up@@ -100,6 +144,8 @@ future fix. Don't pin it, don't copy the logic in.
| `strict` | `false` | also apply `scripts/patterns-strict.txt` (vendor keywords, `.claude/` paths) |
| `format` | `true` | conventional-commit subjects |
| `secrets` | `true` | credential scan on added lines |
| `agent_files` | `true` | refuse an agent's tooling configuration tracked in the tree |
| `agent_files_allow` | *(empty)* | paths exempt from that check, space or comma separated. A bare entry exempts the whole subtree: `.claude` covers `.claude/settings.json` |
| `token` | `github.token` | checkout token |

## Making it mandatory — and irremovable
Expand DownExpand Up@@ -184,6 +230,7 @@ action.yml the action (composite): checkout → resolve → g
scripts/post-commit.sh the gate
scripts/patterns.txt default forbidden patterns (attribution-shaped)
scripts/patterns-strict.txt opt-in keyword patterns
scripts/agent-paths.txt agent tooling paths (.claude/, .cursor/, …); editor config kept
scripts/enforce.sh fleet: stub PR + ruleset, idempotent
scripts/rewrite-history.sh history scrub (messages + identities), dry-run by default
stub/post-commit.yml the file installed in each repo
Expand All@@ -195,6 +242,7 @@ tests/run.sh behaviour tests against real throwaway repos

## Adding a rule

Edit `scripts/patterns.txt`, add a case to `tests/run.sh`, open a PR — this
repo gates itself with the version under review. Once on `main`, the rule is
live everywhere on the next run.
Edit `scripts/patterns.txt` for a rule about what a commit *says*, or
`scripts/agent-paths.txt` for one about what it *tracks*. Add a case to
`tests/run.sh`, open a PR — this repo gates itself with the version under
review. Once on `main`, the rule is live everywhere on the next run.
23 changes: 21 additions & 2 deletions action.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,8 +5,9 @@
name: post-commit
description: >-
Mandatory merge gate: every commit authored by an allowed account, no AI
attribution anywhere in history, conventional commit subjects, no
credentials added. Logic lives in kodflow/post-commit —
attribution anywhere in history, no agent configuration tracked in the tree,
conventional commit subjects, no credentials added. Logic lives in
kodflow/post-commit —
callers pin @main so a fix there is live everywhere on the next run.
branding:
icon: shield
Expand DownExpand Up@@ -35,6 +36,22 @@ inputs:
secrets:
description: Scan the range's added lines for credential shapes.
default: 'true'
agent_files:
description: >-
Refuse an AI agent's tooling configuration tracked in the repository —
.claude/, .cursor/, .windsurf/, .aider.*, .codex/, … (full list:
scripts/agent-paths.txt). The tree at the head is what is read, not the
range, so the check stays red until the files are gone; one `git rm
--cached` clears it, no history rewrite. Editor configuration (.vscode/,
.idea/), .devcontainer/ and markdown instructions (CLAUDE.md, AGENTS.md)
are never matched.
default: 'true'
agent_files_allow:
description: >-
Space/comma separated paths exempt from the agent-artefact check, for a
repository whose purpose is to distribute that configuration. A bare
entry exempts the whole subtree: `.claude` covers `.claude/settings.json`.
default: ''
token:
description: Token used to check out the calling repository.
default: ${{ github.token }}
Expand DownExpand Up@@ -137,6 +154,8 @@ runs:
PC_HISTORY: ${{ inputs.history }}
PC_FORMAT: ${{ inputs.format }}
PC_SECRETS: ${{ inputs.secrets }}
PC_AGENT_FILES: ${{ inputs.agent_files }}
PC_AGENT_ALLOW: ${{ inputs.agent_files_allow }}
PC_REPORT: ${{ runner.temp }}/post-commit-report.md
run: |
rc=0
Expand Down
105 changes: 105 additions & 0 deletions scripts/agent-paths.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
# Agent artefact paths — one ERE per line, matched case-insensitively against
# every path tracked at the scanned head. Blank lines and #-comments ignored.
#
# Single source of truth for the fleet: a rule added here is live in every
# repo pinned to kodflow/post-commit@main on its next run.
#
# DESIGN — a tracked PATH, not a mention. `.claude/` had to stay opt-in in
# patterns.txt for a precise reason: a commit MESSAGE naming it is ordinary
# work ("fix: update .claude/scripts/…"), and 25 of devcontainer-template's
# last 60 legitimate subjects match. A tracked path is the opposite case. The
# string `.claude/settings.json` in the tree IS the artefact; there is nothing
# to misread, no prose to be fair to. Same characters, no false-positive
# problem — so this file is a default while patterns-strict.txt is not.
#
# The remedy differs too, and that is why the scope can afford to be the whole
# tree. An attribution lives in a commit and only a history rewrite removes it.
# A tracked file is removed by removing it:
#
# git rm -r --cached .claude && echo '.claude/' >> .gitignore && commit
#
# The check reads the tree at the head, never the ancestry, so that one commit
# clears it for good.
#
# SCOPE — an agent's tooling configuration. Deliberately NOT matched, and by
# decision rather than by omission:
#
# · .vscode/ .idea/ .zed/ .fleet/ .helix/ — editor configuration. An editor
# is not an agent, and a repository's editor settings long predate all of
# this. Whatever else changes here, these stay out.
# · .devcontainer/ — the environment definition. Humans and CI read it too.
# Note the exact claim: nothing is matched BECAUSE it sits under
# .devcontainer/. An agent directory nested there is still an agent
# directory — the fleet's own template ships one at
# .devcontainer/images/.claude/ — and exempting a parent would make
# "put it under .devcontainer/" the way around every rule in this file.
# · CLAUDE.md AGENTS.md GEMINI.md .github/copilot-instructions.md — markdown
# instructions read as project documentation, and 21 fleet repositories
# carry one. Whether that prose belongs in a repository is an editorial
# call; a gate is the wrong place to make it.
#
# A repository whose purpose IS to distribute this configuration —
# devcontainer-template — exempts itself with the `agent_files_allow` input
# rather than by weakening the list here.

# --- Claude Code ------------------------------------------------------------
(^|/)\.claude/
(^|/)\.claude\.json$
(^|/)\.claudeignore$
# The project-level MCP server list: agent tooling by definition. Bare
# `mcp.json` is NOT matched — it is the ordinary config file name of every
# repository that implements an MCP server, several of which are ours.
(^|/)\.mcp\.json$
# --- Cursor -----------------------------------------------------------------
(^|/)\.cursor/
(^|/)\.cursorrules$
(^|/)\.cursorignore$
(^|/)\.cursorindexingignore$
# --- OpenAI Codex -----------------------------------------------------------
(^|/)\.codex/
# --- GitHub Copilot ---------------------------------------------------------
(^|/)\.copilot/
# --- Gemini CLI / Code Assist -----------------------------------------------
(^|/)\.gemini/
(^|/)\.aiexclude$
# --- Windsurf / Codeium -----------------------------------------------------
(^|/)\.windsurf/
(^|/)\.windsurfrules$
(^|/)\.codeium/
(^|/)\.codeiumignore$
# --- aider ------------------------------------------------------------------
# One pattern for the lot: aider prefixes every dropping it leaves behind —
# .aider.chat.history.md, .aider.input.history, .aider.tags.cache.v3/,
# .aider.conf.yml — with the same string.
(^|/)\.aider[.-]
(^|/)\.aiderignore$
# --- Cline / Roo Code / Kilo Code -------------------------------------------
(^|/)\.cline/
# .clinerules is a file in older versions and a directory in newer ones.
(^|/)\.clinerules(/|$)
(^|/)\.roo/
(^|/)\.roomodes$
(^|/)\.kilocode/
# --- Continue ---------------------------------------------------------------
(^|/)\.continue/
# --- Goose ------------------------------------------------------------------
(^|/)\.goose/
(^|/)\.goosehints$
# --- opencode / Crush / Trae / Junie / Augment ------------------------------
(^|/)\.opencode/
(^|/)\.crush/
(^|/)\.trae/
(^|/)\.junie/
(^|/)\.augment/
(^|/)\.augmentignore$
# --- Amazon Q Developer / Kiro ----------------------------------------------
(^|/)\.amazonq/
(^|/)\.kiro/
# --- Qodo / SpecStory / Devin / Tabnine / Plandex ---------------------------
(^|/)\.qodo/
(^|/)\.specstory/
(^|/)\.devin/
(^|/)\.tabnine/
(^|/)\.plandex/
# --- JetBrains AI -----------------------------------------------------------
(^|/)\.aiignore$
Loading