[AI] Add cross-platform hook adding 🤖 emoji to comments (#8331)

* [AI] Add cross-platform hook keeping GitHub comments/issues in Chinese or pirate voice

Adds a shared agent hook that requires anything an agent posts to GitHub
(PR/issue comments, PR reviews, created issues) to be written in 简体中文
(preferred) or, failing that, a fun pirate voice — never plain English.

- scripts/agent-hooks/github-comment-style.sh: shared guard. Reads
  tool_input.body/.title, allows CJK or pirate-flavoured text, blocks plain
  English via exit 2. Fails open on malformed payloads.
- Wired for Claude (.claude/settings.json PreToolUse), Codex
  (.codex/config.toml PreToolUse) and Cursor (beforeMCPExecution adapter).
- Documents the rule in AGENTS.md, the canonical pr-and-commit-rules.md and
  the Cursor rule so platforms without hook support apply it manually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Use a slug for the release-note filename; document slug naming in AGENTS.md

Release-note filenames don't need to be the PR number — a short descriptive
slug works too (the PR link is resolved at release time). Rename the new note
accordingly and note this in AGENTS.md's directory reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [autofix.ci] apply automated fixes

* [AI] Auto-label PRs with Chinese descriptions as "ai spam" via CodeRabbit

Add a CodeRabbit labeling instruction (auto_apply_labels is already on) that
applies the "ai spam" label when a PR description contains Chinese/CJK
characters — a strong signal of AI-generated spam for this English-language
project.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Exempt CodeRabbit-addressed comments from the Chinese/pirate voice hook

CodeRabbit parses its commands (@coderabbitai review/resolve/etc.) as plain
English, so the github-comment-style guard now skips any comment mentioning
@coderabbitai / @coderabbit instead of forcing it into Chinese or pirate.
Documented the exception in the canonical and Cursor rule files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Key CodeRabbit voice exemption on authorship, not mentions

The hook only ever runs on the agent's own outgoing comments, so CodeRabbit's
own comments are already out of scope (it posts under its own identity). Drop
the @coderabbitai mention bypass — a comment merely name-dropping the bot is
still the agent's prose and must follow the Chinese/pirate rule, closing an
easy plain-English bypass. Docs updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Pin CodeRabbit reviews to English

CodeRabbit was reading AGENTS.md / pr-and-commit-rules.md (which ask
contributors to comment in Chinese/pirate) and applying that to its own
reviews, posting them in Chinese. Set `language: en-US` and `tone_instructions`
so the bot's own reviews, summaries and replies stay in English; the
Chinese/pirate rule is for contributor/agent comments, not CodeRabbit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Replace Chinese/pirate comment voice with a robot-emoji prefix

Scrap the 简体中文/pirate voice scheme (and its CodeRabbit language override and
Chinese-based "ai spam" label) in favour of one simple rule: every GitHub
comment, review or issue an agent posts must be prefixed with 🤖. The shared
guard now blocks any body — or, for issues, title — that doesn't start with the
robot emoji; docs and per-platform wiring updated to match. .coderabbit.yaml is
restored to its original state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Silence SC1007 false positive on CDPATH= cd in Cursor MCP adapter

shellcheck flags the intentional `CDPATH= cd` empty-env prefix as SC1007
(mistaking it for an assignment). Add a scoped inline disable directive on that
line; behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Fail closed on guard execution errors in Cursor MCP adapter

The adapter previously allowed the call on any non-0/2 exit from
github-comment-style.sh, so a broken or missing guard would silently disable
enforcement. Deny on unexpected exits instead (matching guard-shell.sh), with a
message that marks it as an execution problem rather than a real policy denial.
The shared guard still fails open on malformed payloads (its own exit-0 choice).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Add hook requiring a blank PR template when agents create PRs

New shared guard pr-template-blank.sh blocks mcp__github__create_pull_request
unless the body is the repo's PR template, unmodified (cosmetic whitespace
aside) — agents must leave it blank for the human, per AGENTS.md. Wired for
Claude (PreToolUse), Codex (PreToolUse) and Cursor (the beforeMCPExecution
adapter now dispatches per-tool to the right guard). Docs and a release note
updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lqu3eZi5djm2cPAEm3yVht

* [AI] Simplify agent-hook guard scripts

- github-comment-style.sh: fold the empty/whitespace-only case into the
  prefix check, dropping a redundant tr subshell per field.
- pr-template-blank.sh: drop the dead CR strip in canon() (the trailing
  whitespace sub already removes a trailing carriage return).
- before-mcp-github.sh: emit the constant allow payload with printf instead
  of spawning jq on the common (allow) path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] Drop duplicated PR-template/robot-emoji rules now that hooks enforce them

The PR-template-blank and GitHub-comment robot-emoji-prefix rules are enforced
by cross-platform hooks, so the copies scattered across AGENTS.md and the
Cursor rules file are redundant. Remove them and point at the canonical
pr-and-commit-rules.md, which keeps the full rule (including the PR-template
Chinese exception that no hook can enforce). Also delete the internal
PR-template-guard release note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] Drop "enforced by hooks" wording from the PR/comment rule docs

State the rules plainly without the meta-commentary about hook enforcement.
Keeps the actual rules (don't fill the PR template, prefix GitHub
comments/reviews/issues with 🤖, the Chinese exception, and the
your-own-comments-only note) intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] Trim enforcement meta-commentary; shorten robot-emoji release note

Drop the "isn't enforced automatically" / "handled by tooling" framing from
AGENTS.md and pr-and-commit-rules.md (keeping the rule and the "apply it
yourself" responsibility), and shorten the robot-emoji-prefix release note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] Fail closed on unreadable payloads in the GitHub agent guards

Align the new guards with the git-guard.sh / guard-shell.sh convention: fail
closed when the hook payload can't be read, fail open only on a genuinely
absent optional field.

- github-comment-style.sh: block on invalid JSON / missing / non-object
  .tool_input; allow only an absent body/title within a valid object.
- before-mcp-github.sh: deny on a jq parse failure / missing .tool_name
  instead of falling through to allow.
- pr-template-blank.sh: an absent/null body now fails open (previously it
  normalized to "" and wrongly blocked PR creation); an empty-string body is
  still treated as a real non-template submission.
- .codex/config.toml: describe the full guard scope (comment body + issue
  title) in the hook comment and statusMessage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Matiss Janis Aboltins
2026-06-25 23:06:04 +01:00
committed by GitHub
parent 104631b826
commit 7c005ffc50
10 changed files with 240 additions and 14 deletions

View File

@@ -33,6 +33,24 @@
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/no-strict-ignore-new-file.sh\""
}
]
},
{
"matcher": "mcp__github__(add_issue_comment|add_comment_to_pending_review|add_reply_to_pull_request_comment|pull_request_review_write|issue_write|sub_issue_write)",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/github-comment-style.sh\""
}
]
},
{
"matcher": "mcp__github__create_pull_request",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/pr-template-blank.sh\""
}
]
}
],
"Stop": [

View File

@@ -42,6 +42,27 @@ command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/format-edited-f
type = "command"
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/prefer-one-component.sh"'
# Require GitHub comments, reviews and issues to be prefixed with the robot emoji
# 🤖. Matches the github MCP comment/review/issue writers below (add_issue_comment,
# issue_write, …); the guard reads tool_input.body and, for issues,
# tool_input.title (the same payload Claude passes), and blocks an un-prefixed
# comment body or issue title via exit 2.
[[hooks.PreToolUse]]
matcher = "^mcp__github__(add_issue_comment|add_comment_to_pending_review|add_reply_to_pull_request_comment|pull_request_review_write|issue_write|sub_issue_write)$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/github-comment-style.sh"'
statusMessage = "Checking GitHub comment/issue is 🤖-prefixed"
# When creating a PR, require the body to be the blank PR template (unmodified)
# — agents must not fill it in. Reads tool_input.body; blocks via exit 2.
[[hooks.PreToolUse]]
matcher = "^mcp__github__create_pull_request$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/pr-template-blank.sh"'
statusMessage = "Checking PR template is left blank"
# When the turn ends, typecheck + test the workspaces this branch touched.
[[hooks.Stop]]
[[hooks.Stop.hooks]]

View File

@@ -2,6 +2,9 @@
"version": 1,
"hooks": {
"beforeShellExecution": [{ "command": "./.cursor/hooks/guard-shell.sh" }],
"beforeMCPExecution": [
{ "command": "./.cursor/hooks/before-mcp-github.sh" }
],
"afterFileEdit": [{ "command": "./.cursor/hooks/after-file-edit.sh" }]
}
}

View File

@@ -0,0 +1,57 @@
#!/bin/sh
# Cursor `beforeMCPExecution` adapter.
#
# Dispatches github MCP calls to the matching shared guard in
# scripts/agent-hooks/: comment/review/issue writers must be 🤖-prefixed
# (github-comment-style.sh), and create_pull_request must leave the PR template
# blank (pr-template-blank.sh).
# Cursor input on stdin: { tool_name, tool_input, ... }. Output on stdout:
# { permission: "allow" | "deny", userMessage, agentMessage }.
input=$(cat)
# `CDPATH= cd` is an intentional empty-env prefix (not an assignment); silence the
# SC1007 false positive shellcheck raises for it.
# shellcheck disable=SC1007
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
allow() {
printf '%s\n' '{"permission":"allow"}'
exit 0
}
deny() {
jq -n --arg m "$1" \
'{permission:"deny", userMessage:"Blocked by repo policy", agentMessage:$m}'
exit 0
}
# Pick the guard for this tool; wave known non-GitHub tools by. Fail closed on a
# malformed payload (jq parse failure / missing .tool_name), matching
# guard-shell.sh — a payload we can't read must not bypass the GitHub guards.
tool=$(printf '%s' "$input" | jq -re '.tool_name' 2>/dev/null) ||
deny "Invalid Cursor hook payload: could not read .tool_name."
case "$tool" in
mcp__github__add_issue_comment | \
mcp__github__add_comment_to_pending_review | \
mcp__github__add_reply_to_pull_request_comment | \
mcp__github__pull_request_review_write | \
mcp__github__issue_write | \
mcp__github__sub_issue_write)
guard=github-comment-style.sh ;;
mcp__github__create_pull_request)
guard=pr-template-blank.sh ;;
*) allow ;;
esac
# Each guard reads the same `.tool_input.*` payload Cursor passes, so forward it
# unchanged. Exit 0 allows, exit 2 + stderr is a real policy denial. Fail closed
# on any other exit (matching guard-shell.sh) so a broken/missing guard can't
# silently disable enforcement; the message marks it as an execution problem.
# (Each guard fails closed on an unreadable payload and exit-0s only when there's
# genuinely nothing to enforce.)
status=0
err=$(printf '%s' "$input" | "$ROOT/scripts/agent-hooks/$guard" 2>&1) || status=$?
case "$status" in
0) allow ;;
2) deny "$err" ;;
*) deny "$guard failed (exit $status): $err" ;;
esac

View File

@@ -14,6 +14,3 @@ listed here.
- **ALL pull request titles MUST be prefixed with `[AI]`.** This isn't enforced
automatically — you have to apply it yourself.
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`) — leave it
blank for the human. Exception: if a human explicitly asks, fill it out **in
Chinese** (简体中文).

View File

@@ -1,14 +1,13 @@
# PR and Commit Rules for AI Agents
The mechanical commit and git rules are handled by tooling. This file lists only
the rules you have to apply yourself.
This file lists the PR and commit rules you have to apply yourself.
## Pull Request Rules
### `[AI]` prefix on PR titles
**ALL pull request titles MUST be prefixed with `[AI]`.** This isn't enforced
automatically — you have to apply it yourself.
**ALL pull request titles MUST be prefixed with `[AI]`** — you have to apply it
yourself.
**Examples:**
@@ -17,8 +16,24 @@ automatically — you have to apply it yourself.
### Do not fill in the PR template
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all
blank spaces and placeholder comments as-is — the human who tested the change
fills in the Description, Related issue(s), Testing, and Checklist sections.
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Create
the PR with that template as the body, unmodified — leave all blank spaces and
placeholder comments as-is, and leave every checklist box unchecked. The human
who tested the change fills in the Description, Related issue(s), Testing, and
Checklist sections.
- **Exception**: if a human **explicitly asks** you to fill it out, do so **in
Chinese**, using Chinese characters (简体中文) for all content you add.
## GitHub comment, review and issue prefix
**Prefix everything you post to GitHub with the robot emoji 🤖** — pull-request
and issue comments, pull-request reviews (including inline review comments), and
the title and body of issues you create. This keeps agent-authored content
visibly marked. It does **not** change PR titles (still `[AI] …`) or commit
messages (still `[AI] …`).
Write the text normally; just make sure 🤖 is the first character (for issues,
on both the title and the body).
This applies only to comments **you** author — bots like CodeRabbit post under
their own identity and are not affected.

View File

@@ -44,8 +44,8 @@ yarn start:desktop
### ⚠️ PR titles must start with `[AI]`
Every pull request title must be prefixed with `[AI]`this isn't enforced
automatically, so you have to apply it yourself. See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md).
Every pull request title must be prefixed with `[AI]`you have to apply it
yourself. See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md).
### Task Orchestration with Lage
@@ -357,7 +357,11 @@ describe('ComponentName', () => {
- `/README.md` - Project overview
- `/CONTRIBUTING.md` - Points to community docs
- `/upcoming-release-notes/` - Release notes for next version
- `/upcoming-release-notes/` - Release notes for next version. Name each file
with a short, descriptive slug (e.g. `add-payee-autocomplete.md`) — the PR link
is resolved automatically at release time, so you don't need the PR number.
Numeric filenames like `1234.md` also remain valid. See the release-note
template and rules in `packages/docs/docs/contributing/index.md`.
- `/CODEOWNERS` - Code ownership definitions
- `/packages/docs/` - Documentation website (Docusaurus)
@@ -506,7 +510,7 @@ Before committing changes, ensure:
## Pull Request Guidelines
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, and PR template handling.
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, the GitHub comment/review/issue 🤖 prefix, and PR template handling.
## Code Review Guidelines

View File

@@ -0,0 +1,51 @@
#!/bin/sh
# Shared agent guard for GitHub comments / reviews / issues, wired for Claude
# (PreToolUse), Codex (PreToolUse) and Cursor (beforeMCPExecution via adapter).
#
# Every comment, review or issue an agent posts to GitHub must be prefixed with
# the robot emoji 🤖, so agent-authored content is always visibly marked. Reads
# the text from `.tool_input.body` (and `.tool_input.title` for issues) on
# stdin; each non-empty field must start with 🤖 (after any leading whitespace),
# otherwise the call is blocked (exit 2 + stderr) so the agent fixes it.
#
# This only ever runs on the agent's own outgoing comments — bots like CodeRabbit
# post under their own identity and never pass through this hook.
block() {
echo "$1" >&2
exit 2
}
input=$(cat)
# 🤖 = U+1F916, UTF-8 F0 9F A4 96. Build it from raw bytes so the prefix check is
# locale- and file-encoding-independent.
robot=$(printf '\360\237\244\226')
# Fail closed on a malformed payload — invalid JSON, or a missing/non-object
# `.tool_input` — matching git-guard.sh / guard-shell.sh, so a payload we can't
# read can't silently bypass the prefix check. A genuinely absent body/title
# within a valid `.tool_input` is fine (nothing to mark).
fields=$(printf '%s' "$input" | jq -e '.tool_input | objects' 2>/dev/null) ||
block "Blocked: could not read the hook payload (.tool_input). (scripts/agent-hooks/github-comment-style.sh)"
body=$(printf '%s' "$fields" | jq -r '.body // empty')
title=$(printf '%s' "$fields" | jq -r '.title // empty')
# A field passes if it's empty/whitespace-only (nothing to mark) or, after
# stripping leading whitespace, begins with the robot emoji.
starts_with_robot() {
# ${1%%[![:space:]]*} is the leading whitespace; strip it off the front. A
# whitespace-only (or empty) field strips down to "" and passes too.
trimmed=${1#"${1%%[![:space:]]*}"}
case "$trimmed" in
"" | "$robot"*) return 0 ;;
*) return 1 ;;
esac
}
starts_with_robot "$title" ||
block "Blocked: GitHub issue titles must start with the robot emoji 🤖 to mark them as agent-authored. Prefix the title with 🤖 and try again. (scripts/agent-hooks/github-comment-style.sh)"
starts_with_robot "$body" ||
block "Blocked: GitHub comments, reviews and issues must start with the robot emoji 🤖 to mark them as agent-authored. Prefix the body with 🤖 and try again. (scripts/agent-hooks/github-comment-style.sh)"
exit 0

View File

@@ -0,0 +1,54 @@
#!/bin/sh
# Shared agent guard for pull-request creation, wired for Claude (PreToolUse),
# Codex (PreToolUse) and Cursor (beforeMCPExecution via adapter).
#
# Agents must NOT fill in the PR template: when creating a pull request the body
# must be the repo's blank template (.github/PULL_REQUEST_TEMPLATE.md),
# unmodified, so the human who tested the change fills in the Description,
# Testing and Checklist sections (AGENTS.md / pr-and-commit-rules.md). Reads
# `.tool_input.body` on stdin and blocks (exit 2 + stderr) if it isn't the
# pristine template.
#
# Comparison ignores cosmetic differences only: CR/LF, trailing whitespace per
# line, and leading/trailing blank lines. Any real content (a filled-in section,
# a checked box, extra prose) makes the body differ and is blocked.
block() {
echo "$1" >&2
exit 2
}
input=$(cat)
# Resolve the repo root regardless of which agent invoked us, then the template.
. "$(dirname "$0")/common.sh"
ROOT=$(resolve_repo_root)
template="$ROOT/.github/PULL_REQUEST_TEMPLATE.md"
# If we can't find the template, fail open — never block PR creation just because
# the comparison baseline is missing.
[ -f "$template" ] || exit 0
# Fail open when there's no body to compare: a parse failure or an absent/null
# `.tool_input.body` must not wedge PR creation. (The Cursor/Codex/Claude wiring
# still fails closed if THIS script can't execute at all.) `jq -re` exits
# non-zero for both cases; an empty-string body, however, is a real submission
# and still falls through to the template check below.
body=$(printf '%s' "$input" | jq -re '.tool_input.body' 2>/dev/null) || exit 0
# Canonicalise: drop CR, strip per-line trailing whitespace, and trim leading and
# trailing blank lines, so only meaningful content differences remain.
canon() {
printf '%s\n' "$1" | awk '
{ sub(/[[:space:]]+$/, ""); lines[NR] = $0 } # also strips a trailing CR
END {
s = 1; e = NR
while (s <= e && lines[s] == "") s++
while (e >= s && lines[e] == "") e--
for (i = s; i <= e; i++) print lines[i]
}'
}
[ "$(canon "$body")" = "$(canon "$(cat "$template")")" ] && exit 0
block "Blocked: don't fill in the PR template. Create the pull request with the repo's blank template (.github/PULL_REQUEST_TEMPLATE.md) as the body, unmodified — the human who tested the change fills in the Description, Testing and Checklist sections (AGENTS.md). (scripts/agent-hooks/pr-template-blank.sh)"

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Prefix agent-authored GitHub comments, reviews and issues with the robot emoji 🤖.