Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-12
20 changes: 20 additions & 0 deletions openspec/changes/strip-cursor-attribution-locally/design.md
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions openspec/changes/strip-cursor-attribution-locally/proposal.md
Original file line number Diff line number Diff line change
@@ -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 <cursoragent@cursor.com>` 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
Original file line number Diff line number Diff line change
@@ -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 <cursoragent@cursor.com>` 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
4 changes: 4 additions & 0 deletions openspec/changes/strip-cursor-attribution-locally/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 25 additions & 1 deletion openspec/specs/code-quality-tooling/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cursoragent@cursor.com>` 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.
Expand Down Expand Up @@ -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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
32 changes: 32 additions & 0 deletions scripts/strip-cursor-coauthor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { readFileSync, writeFileSync } from 'node:fs'
import process from 'node:process'

const cursorCoAuthorPattern = /^Co-authored-by:\s*Cursor(?: Agent)? <cursoragent@cursor\.com>\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.')
}
}
37 changes: 37 additions & 0 deletions test/strip-cursor-coauthor.test.ts
Original file line number Diff line number Diff line change
@@ -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 <cursoragent@cursor.com>',
'',
].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 <person@example.com>', ''].join('\n')

expect(stripCursorAttributionTrailers(message)).toBe(message)
})

it('accepts the shorter Cursor name variant case-insensitively', () => {
expect(
stripCursorAttributionTrailers(
['chore: example', '', 'co-authored-by: cursor <cursoragent@cursor.com>'].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'),
)
})
})
Loading