diff --git a/.cursor/rules/git-commits.mdc b/.cursor/rules/git-commits.mdc new file mode 100644 index 0000000..6ca4d8f --- /dev/null +++ b/.cursor/rules/git-commits.mdc @@ -0,0 +1,50 @@ +--- +description: Agent commits and PRs - plain git commit with hooks installed, no Cursor attribution +alwaysApply: true +--- + +# Git Commits + +Commit messages and pull requests contain only what the user asked for. Never add +`Co-authored-by:` trailers or any other agent attribution. + +Cursor appends a `Co-authored-by: Cursor` trailer to every agent `git commit`, whatever the +settings or rules say. The `strip-cursor-coauthor` commit-msg hook removes it before the +commit is created, so commit normally and let the hooks run. + +## Committing + +1. Once per checkout, make sure the commit-msg hook is installed. A plain install covers + every stage listed in `default_install_hook_types`: + + ```bash + test -x "$(git rev-parse --git-path hooks)/commit-msg" || uv run pre-commit install + ``` + +2. Commit with `git commit`. Never pass `--no-verify`: it skips the hook that removes the + trailer, along with the message checks. +3. Verify with `git log -1 --format=full`. If the trailer is still there, the hook did not + run: install it (step 1), then `git commit --amend --no-edit` while the commit is unpushed. + +## Fallback: no hooks + +If installing the hooks is not an option, build the commit with `git commit-tree`, which +Cursor leaves alone. It skips every hook too, so run them yourself first: + +```bash +uv run pre-commit run --files $(git diff --cached --name-only) +uv run pre-commit run --hook-stage commit-msg --commit-msg-filename /tmp/commit-msg +old=$(git rev-parse HEAD) +new=$(git commit-tree "$(git write-tree)" -p "$old" < /tmp/commit-msg) +git update-ref "refs/heads/$(git branch --show-current)" "$new" "$old" +``` + +Passing the old tip makes `update-ref` fail if the branch moved in the meantime. + +## Pull requests + +- Never append `Made with Cursor`, `Made with [Cursor](https://cursor.com)`, or similar + attribution to PR titles or bodies. +- PR descriptions contain only **Summary** and **Test plan**, or what the user asked for. +- After `gh pr create` or `gh pr edit`, check `gh pr view --json body` and remove + any injected footer. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ed07d0..30bc4d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,6 +51,13 @@ repos: # made without hooks (git commit-tree, --no-verify). - repo: local hooks: + # Runs first so later commit-msg checks never see the injected trailer. + - id: strip-cursor-coauthor + name: strip Cursor co-author trailer + entry: tools/shell/strip-cursor-coauthor.sh + language: script + stages: [commit-msg] + pass_filenames: true - id: jargon-commit-msg name: jargon (commit message) entry: node --disable-warning=ExperimentalWarning tools/typescript/essentials-sync/src/jargon-check.ts --message-file diff --git a/tools/shell/strip-cursor-coauthor.sh b/tools/shell/strip-cursor-coauthor.sh new file mode 100755 index 0000000..a0297f8 --- /dev/null +++ b/tools/shell/strip-cursor-coauthor.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Remove the `Co-authored-by: Cursor` trailer from a commit message. +# +# Cursor appends the trailer while composing the message, before git runs any +# hook. `--no-verify` does not suppress it and `git commit --amend` re-adds it, +# so a commit-msg hook is the only place that reliably removes it without +# rewriting the commit afterwards. The IDE toggle (Cursor Settings -> Git & +# Pull Requests -> Attribution) covers the local case; this hook also covers +# contributors who have not set it and surfaces that Cloud Agents ignore it. +# +# Matches the agent's mailbox rather than its display name: a human +# contributor could be called "Cursor", but only the bot commits from this +# address. Human `Co-authored-by:` lines are left untouched. + +set -euo pipefail + +readonly CURSOR_TRAILER='^Co-authored-by:.*' + +msg_file="${1:?usage: strip-cursor-coauthor.sh }" + +if ! grep -qiE "$CURSOR_TRAILER" "$msg_file"; then + exit 0 +fi + +stripped="$(mktemp)" +trap 'rm -f "$stripped"' EXIT + +# `|| true`: grep exits 1 when it filters out every line, which `set -e` +# would otherwise treat as a failure. +grep -viE "$CURSOR_TRAILER" "$msg_file" >"$stripped" || true + +# The command substitution drops the blank line the trailer left behind. +printf '%s\n' "$(cat "$stripped")" >"$msg_file"