From 606ce4ff625391da2bedf080619fdf8fb839272f Mon Sep 17 00:00:00 2001 From: drswith <540628938@qq.com> Date: Tue, 12 May 2026 20:59:54 +0800 Subject: [PATCH] chore(git): strip cursor attribution trailers locally --- .../.openspec.yaml | 2 + .../design.md | 20 ++++++++++ .../proposal.md | 21 +++++++++++ .../specs/code-quality-tooling/spec.md | 26 +++++++++++++ .../strip-cursor-attribution-locally/tasks.md | 4 ++ openspec/specs/code-quality-tooling/spec.md | 26 ++++++++++++- package.json | 1 + scripts/strip-cursor-coauthor.ts | 32 ++++++++++++++++ test/strip-cursor-coauthor.test.ts | 37 +++++++++++++++++++ 9 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/strip-cursor-attribution-locally/.openspec.yaml create mode 100644 openspec/changes/strip-cursor-attribution-locally/design.md create mode 100644 openspec/changes/strip-cursor-attribution-locally/proposal.md create mode 100644 openspec/changes/strip-cursor-attribution-locally/specs/code-quality-tooling/spec.md create mode 100644 openspec/changes/strip-cursor-attribution-locally/tasks.md create mode 100644 scripts/strip-cursor-coauthor.ts create mode 100644 test/strip-cursor-coauthor.test.ts diff --git a/openspec/changes/strip-cursor-attribution-locally/.openspec.yaml b/openspec/changes/strip-cursor-attribution-locally/.openspec.yaml new file mode 100644 index 0000000..40cc12f --- /dev/null +++ b/openspec/changes/strip-cursor-attribution-locally/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-12 diff --git a/openspec/changes/strip-cursor-attribution-locally/design.md b/openspec/changes/strip-cursor-attribution-locally/design.md new file mode 100644 index 0000000..4be6b4b --- /dev/null +++ b/openspec/changes/strip-cursor-attribution-locally/design.md @@ -0,0 +1,20 @@ +## Summary + +Implement the local mitigation at the `commit-msg` stage using the repository's existing `simple-git-hooks` installation path. + +## Decision + +Use a repository-owned Bun/TypeScript script invoked from `simple-git-hooks` `commit-msg` rather than a shell-only one-liner. + +## Rationale + +- portability: one Bun script works the same on macOS, Linux, and Windows shells used by contributors +- versioned behavior: the exact strip rule lives in the repo and can be tested with Vitest +- narrow scope: the script can intentionally remove only Cursor's known local attribution trailer formats, avoiding accidental policy drift from the broader CI validator + +## Non-Goals + +- replacing CI or PR governance checks with a local hook +- stripping arbitrary non-Cursor `Co-authored-by:` trailers from local commits +- stripping arbitrary non-Cursor branding or provenance trailers beyond Cursor's known local formats +- fixing hosted cloud-agent server-side metadata injection that happens outside local Git hook execution diff --git a/openspec/changes/strip-cursor-attribution-locally/proposal.md b/openspec/changes/strip-cursor-attribution-locally/proposal.md new file mode 100644 index 0000000..beb1df8 --- /dev/null +++ b/openspec/changes/strip-cursor-attribution-locally/proposal.md @@ -0,0 +1,21 @@ +## Why + +Quantex already has remote governance that rejects prohibited `Co-authored-by:` trailers and risky pull-request commit metadata before merge. That protects protected branches, but it does not stop local developer workflows from repeatedly generating Cursor-specific co-author trailers that then have to be removed by hand before every commit or fixed after CI failures. + +The local problem is narrower than the remote policy: + +- local IDE, CLI, and generated commit-message flows can inject a known Cursor co-author trailer +- the existing remote governance intentionally stays broader than Cursor and must not be weakened to a tool-specific rule +- contributors want a repository-native fix that travels with the repo through `simple-git-hooks` + +## What Changes + +- add a versioned repository `commit-msg` hook through `simple-git-hooks` +- implement a repository script that strips Cursor's known local attribution trailers (`Co-authored-by: Cursor Agent ` and `Made-with: Cursor`) from the commit message file before commit creation +- document in OpenSpec that the local hook is Cursor-specific while CI governance remains generic + +## Impact + +- local commits created from clones with hooks installed stop carrying Cursor's co-author trailer by default +- CI and PR governance remain the final generic enforcement layer for non-Cursor `Co-authored-by:` trailers and risky author metadata +- cloud-hosted agent flows that inject metadata after local hooks are still governed remotely rather than by this local hook diff --git a/openspec/changes/strip-cursor-attribution-locally/specs/code-quality-tooling/spec.md b/openspec/changes/strip-cursor-attribution-locally/specs/code-quality-tooling/spec.md new file mode 100644 index 0000000..27e2129 --- /dev/null +++ b/openspec/changes/strip-cursor-attribution-locally/specs/code-quality-tooling/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Local commit-msg hook MUST remove Cursor attribution trailers before commit creation + +The repository SHALL enforce a versioned `commit-msg` hook through `simple-git-hooks` that strips the exact Cursor-injected attribution trailer lines from the commit message file before Git finalizes a local commit. This local hook MUST target Cursor's known local attribution formats only and MUST NOT narrow or replace the broader remote co-author governance enforced by CI. + +#### Scenario: Local commit message contains a Cursor co-author trailer + +- **GIVEN** a local IDE, CLI agent, or commit-message generator writes `Co-authored-by: Cursor Agent ` into the commit message file +- **WHEN** the repository `commit-msg` hook runs +- **THEN** the hook removes that trailer line before Git creates the commit +- **AND** the contributor does not need to hand-edit the generated message to satisfy local authorship policy + +#### Scenario: Local commit message contains a Cursor made-with trailer + +- **GIVEN** a local IDE, CLI agent, or commit-message generator writes `Made-with: Cursor` into the commit message file +- **WHEN** the repository `commit-msg` hook runs +- **THEN** the hook removes that trailer line before Git creates the commit +- **AND** the contributor does not need to hand-edit the generated message to satisfy local authorship policy + +#### Scenario: Local commit message contains a non-Cursor co-author trailer + +- **GIVEN** a local commit message contains a `Co-authored-by:` trailer for an identity other than Cursor's known trailer identity +- **WHEN** the repository `commit-msg` hook runs +- **THEN** the hook leaves that trailer untouched +- **AND** the existing CI and PR governance checks remain responsible for rejecting prohibited non-Cursor co-author metadata before merge diff --git a/openspec/changes/strip-cursor-attribution-locally/tasks.md b/openspec/changes/strip-cursor-attribution-locally/tasks.md new file mode 100644 index 0000000..a7dc9f5 --- /dev/null +++ b/openspec/changes/strip-cursor-attribution-locally/tasks.md @@ -0,0 +1,4 @@ +- [x] 1. Add a repository-owned `commit-msg` sanitizer script that removes Cursor's known local attribution trailers from local commit messages. +- [x] 2. Wire the sanitizer into the repository `simple-git-hooks` configuration and keep the existing pre-commit and pre-push hooks intact. +- [x] 3. Add automated tests for the sanitizer behavior, including preserving non-Cursor co-author trailers. +- [x] 4. Update the code-quality tooling spec to document the local Cursor-specific hook and its boundary against broader CI governance. diff --git a/openspec/specs/code-quality-tooling/spec.md b/openspec/specs/code-quality-tooling/spec.md index 737e033..aaabea5 100644 --- a/openspec/specs/code-quality-tooling/spec.md +++ b/openspec/specs/code-quality-tooling/spec.md @@ -85,6 +85,31 @@ The repository SHALL enforce lint and format on staged files before each commit - **THEN** the hook does not route that file through an unsupported `oxfmt` invocation - **AND** the commit is not blocked solely because the formatter cannot handle that file type in the current repository configuration +### Requirement: Local commit-msg hook MUST remove Cursor attribution trailers before commit creation + +The repository SHALL enforce a versioned `commit-msg` hook through `simple-git-hooks` that strips the exact Cursor-injected attribution trailer lines from the commit message file before Git finalizes a local commit. This local hook MUST target Cursor's known local attribution formats only and MUST NOT narrow or replace the broader remote co-author governance enforced by CI. + +#### Scenario: Local commit message contains a Cursor co-author trailer + +- **GIVEN** a local IDE, CLI agent, or commit-message generator writes `Co-authored-by: Cursor Agent ` into the commit message file +- **WHEN** the repository `commit-msg` hook runs +- **THEN** the hook removes that trailer line before Git creates the commit +- **AND** the contributor does not need to hand-edit the generated message to satisfy local authorship policy + +#### Scenario: Local commit message contains a Cursor made-with trailer + +- **GIVEN** a local IDE, CLI agent, or commit-message generator writes `Made-with: Cursor` into the commit message file +- **WHEN** the repository `commit-msg` hook runs +- **THEN** the hook removes that trailer line before Git creates the commit +- **AND** the contributor does not need to hand-edit the generated message to satisfy local authorship policy + +#### Scenario: Local commit message contains a non-Cursor co-author trailer + +- **GIVEN** a local commit message contains a `Co-authored-by:` trailer for an identity other than Cursor's known trailer identity +- **WHEN** the repository `commit-msg` hook runs +- **THEN** the hook leaves that trailer untouched +- **AND** the existing CI and PR governance checks remain responsible for rejecting prohibited non-Cursor co-author metadata before merge + ### Requirement: CI lint and format gate CI workflows that gate merges to `main` or `beta` (such as `ci.yml`, `release.yml`, and `release-verify.yml`) SHALL run both `bun run lint` and `bun run format:check`. CI MUST fail when either command exits non-zero. @@ -223,4 +248,3 @@ The repository SHALL keep a root `.editorconfig` that declares LF line endings, - **WHEN** a contributor or coding agent inspects repository formatting and line-ending configuration - **THEN** `.editorconfig`, `.gitattributes`, and `.oxfmtrc.json` all declare LF as the repository text line-ending policy - diff --git a/package.json b/package.json index 894d617..a330b79 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "vitest": "^4.1.3" }, "simple-git-hooks": { + "commit-msg": "bun run scripts/strip-cursor-coauthor.ts \"$1\"", "pre-commit": "bun install --frozen-lockfile && npx lint-staged", "pre-push": "bun run format:check && bun run typecheck && bun run openspec:validate && bun run memory:check" }, diff --git a/scripts/strip-cursor-coauthor.ts b/scripts/strip-cursor-coauthor.ts new file mode 100644 index 0000000..e6592a3 --- /dev/null +++ b/scripts/strip-cursor-coauthor.ts @@ -0,0 +1,32 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import process from 'node:process' + +const cursorCoAuthorPattern = /^Co-authored-by:\s*Cursor(?: Agent)? \s*$/i +const cursorMadeWithPattern = /^Made-with:\s*Cursor\s*$/i + +export function stripCursorAttributionTrailers(message: string): string { + const lines = message.split('\n') + const filteredLines = lines.filter(line => { + const trimmedLine = line.trim() + return !cursorCoAuthorPattern.test(trimmedLine) && !cursorMadeWithPattern.test(trimmedLine) + }) + + return filteredLines.join('\n') +} + +if (import.meta.main) { + const messageFilePath = process.argv[2] + + if (!messageFilePath) { + console.error('Expected the commit message file path as the first argument.') + process.exit(1) + } + + const originalMessage = readFileSync(messageFilePath, 'utf8') + const sanitizedMessage = stripCursorAttributionTrailers(originalMessage) + + if (sanitizedMessage !== originalMessage) { + writeFileSync(messageFilePath, sanitizedMessage, 'utf8') + console.log('Removed Cursor attribution trailer from commit message.') + } +} diff --git a/test/strip-cursor-coauthor.test.ts b/test/strip-cursor-coauthor.test.ts new file mode 100644 index 0000000..4b8c587 --- /dev/null +++ b/test/strip-cursor-coauthor.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' +import { stripCursorAttributionTrailers } from '../scripts/strip-cursor-coauthor' + +describe('stripCursorAttributionTrailers', () => { + it('removes the Cursor co-author trailer', () => { + expect( + stripCursorAttributionTrailers( + [ + 'fix(schema): align doctor JSON schema with Cargo installer field', + '', + 'Co-authored-by: Cursor Agent ', + '', + ].join('\n'), + ), + ).toBe(['fix(schema): align doctor JSON schema with Cargo installer field', '', ''].join('\n')) + }) + + it('leaves other co-author trailers untouched', () => { + const message = ['docs: note authorship', '', 'Co-authored-by: Example Person ', ''].join('\n') + + expect(stripCursorAttributionTrailers(message)).toBe(message) + }) + + it('accepts the shorter Cursor name variant case-insensitively', () => { + expect( + stripCursorAttributionTrailers( + ['chore: example', '', 'co-authored-by: cursor '].join('\n'), + ), + ).toBe(['chore: example', ''].join('\n')) + }) + + it('removes the Made-with Cursor trailer', () => { + expect(stripCursorAttributionTrailers(['feat: example', '', 'Made-with: Cursor', ''].join('\n'))).toBe( + ['feat: example', '', ''].join('\n'), + ) + }) +})