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
1 change: 1 addition & 0 deletions .agents/skills/audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ For every changed file:
| Changed | Check |
| --- | --- |
| Any | `just check` |
| Any commit or PR | `just pr-privacy`, with `PR_TITLE` and the PR body file when a PR exists or is about to be opened; then reread them against the git skill's Privacy section (names are not machine-checked) |
| Rust in a package | `cargo test -p <package>` (SQLX_OFFLINE unset), `cargo fmt --all -- --check`, `just clippy` |
| `sqlx::query!` or migrations | `.sqlx/` regenerated and committed; migration works with the code already deployed |
| `Cargo.toml` | `cargo x deps --check` |
Expand Down
39 changes: 38 additions & 1 deletion .agents/skills/git/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,49 @@ The PR title follows the same form; squash merges use it as the commit on `main`
- **body:** why the change was needed and anything a reviewer can't see in the
diff. Wrap at 72 columns.

## Privacy

Everything written into the repository or onto GitHub is public: PR titles and
bodies, commit messages, review comments and replies, code comments, docs, and
test fixtures. Describe the code change and nothing else.

- Never include personal data: names, emails, phone numbers, usernames,
addresses, account or workspace ids of real users, or @mentions of people.
- Never name customers, prospects, partners, their companies, or their
products. Name a third-party product only when the code integrates with it
(`Slack integration`), never as the source of a request or report.
- Never cite support channels, tickets, chat channels or threads, calls,
meetings, emails, or links into support tools or Macro workspace items
(task links are fine). Don't say who asked or where it was reported.
- Never mention places tied to a person or customer (cities, offices, regions).
- When the reason matters, say it in general terms: "Users with long read
histories saw channels load slowly", not "<customer> reported in
<channel> that…". "A user request" is enough; omit the requester.
- Test fixtures and examples use placeholders (`user@example.com`, `Acme`),
never data copied from a real account or conversation.
- Context the user gives you (a support thread, a customer name, a screenshot)
stays in the conversation; restate it as the general problem before writing.

CI's `PR Privacy Check` fails a PR whose title, body, or commits contain
emails, phone numbers, @mentions, chat-channel names, or support-tool and Macro
workspace links (Dependabot PRs are exempt). It cannot see names, so passing it
is not enough: reread every title, body, and message against the list above.
Run it locally before pushing or opening a PR:

```sh
just pr-privacy # commit messages on this branch
PR_TITLE="fix(x): ..." just pr-privacy body.md # plus the PR title and body you will post
```

To fix a hit, edit the PR title or body, or reword the commit on your branch.

## Before pushing

Run what applies to the files you changed, and fix failures before pushing:

```sh
just check # format, lint, code rules on changed files
just pr-privacy # commit messages carry no personal details
cargo fmt --all -- --check # any Rust change
cargo x deps --check # Cargo.toml or crate added/removed
cargo x workflows --check # workflow generators changed
Expand All @@ -60,7 +97,7 @@ order, and drop any that would be empty:
## Why

The problem or goal, with the evidence: the bug and how it shows up, numbers
measured, the user request. Link issues.
measured. Link GitHub issues or Macro tasks. Follow the Privacy section above.

## How it works

Expand Down
113 changes: 113 additions & 0 deletions .github/workflows/pr-privacy-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# DO NOT EDIT — regenerate with `cargo x workflows` (from the repository root).
# Source: tooling/xtask/crates/xtask_workflows/src/workflows/pr_privacy_check.rs
name: PR Privacy Check
'on':
pull_request:
types:
- opened
- edited
- reopened
- synchronize
jobs:
check:
if: github.event.pull_request.user.login != 'dependabot[bot]'
name: PR text has no personal or support-channel details
runs-on: namespace-profile-linux-tiny-no-cache
permissions:
contents: read
pull-requests: read
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
timeout-minutes: 5
steps:
- name: Scan PR title, body, and commit messages
run: |
# Fail when a PR title, PR body, or commit message carries personal or
# support-channel details. The rule lives in the git skill
# (.agents/skills/git/SKILL.md, "Privacy"); this catches the patterns a
# regex can see: emails, phone numbers, @mentions, chat-channel names, and
# links into support tools or Macro workspace content. Names of people and
# customers can't be matched mechanically; reviewers own those.
#
# CI: PR_TITLE, PR_BODY, PR_NUMBER, GH_REPO, GH_TOKEN; commits come from the
# PR API. Locally (`just pr-privacy [body-file]`): commits are
# CHECK_BASE..HEAD (default: merge-base with origin/main); PR_TITLE and the
# optional body file are scanned when given.
set -euo pipefail

found=0

# report SOURCE LABEL MATCHES: print each match under its source.
report() {
local source="$1" label="$2" matches="$3"
[[ -z "$matches" ]] && return 0
found=1
while IFS= read -r m; do
echo "::error::${source}: ${label}: ${m}"
done <<<"$matches"
}

# scan SOURCE: read text on stdin and report every banned pattern.
scan() {
local source="$1" text prose
# Trailers (Co-authored-by, Signed-off-by) are git identity metadata.
text=$(grep -viE '^[A-Za-z-]+-by: ' || true)
[[ -z "$text" ]] && return 0
# Mentions and channel names are only checked outside code, where
# decorators, CSS colors, and shell comments look alike.
prose=$(awk '/^[[:space:]]*(```|~~~)/ { fenced = !fenced; next } !fenced' <<<"$text" | sed -E 's/`[^`]*`//g')

report "$source" "email address" "$(grep -oE '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' <<<"$text" \
| grep -viE '@(example\.(com|org|net)|([a-z0-9-]+\.)?noreply\.github\.com|(group|resource)\.calendar\.google\.com)$|^git@github\.com$' || true)"
report "$source" "phone number" "$(grep -oE '(\+[0-9]{1,3}[ .-]?)?\(?[0-9]{3}\)?[ .-][0-9]{3}[ .-][0-9]{4}' <<<"$text" || true)"
report "$source" "support or chat link" "$(grep -oiE 'https?://([a-z0-9-]+\.slack\.com/(archives|client|team)|app\.slack\.com/client|app\.intercom\.(com|io)|[a-z0-9-]+\.zendesk\.com/agent|app\.frontapp\.com|discord\.com/channels|mail\.google\.com|app\.hubspot\.com|app\.usepylon\.com|app\.plain\.com|app\.crisp\.chat|secure\.helpscout\.net)[^[:space:])>]*' <<<"$text" || true)"
# Links to workspace items (channels, documents, chats, emails). Tasks are
# the issue tracker and may be linked like GitHub issues.
report "$source" "Macro workspace link" "$(grep -oiE 'https?://([a-z0-9-]+\.)?macro\.com/app/[a-z-]+/[A-Za-z0-9-]+[^[:space:])>`]*' <<<"$text" \
| grep -viE '/app/tasks?/' || true)"
# `@scope/package` names end in a slash and are not mentions.
report "$source" "@mention" "$(grep -oE '(^|[[:space:](])@[A-Za-z0-9][A-Za-z0-9-]*/?' <<<"$prose" \
| sed -E 's/^[[:space:](]*//' | grep -v '/$' \
| grep -viE '^@(mentions?|coderabbitai|dependabot|github-actions|copilot|claude|cursor)$' || true)"
report "$source" "chat channel" "$(grep -oE '(^|[[:space:]])#[a-z][a-z0-9_-]*' <<<"$prose" \
| sed -E 's/^[[:space:]]*//' \
| grep -vE '^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$' || true)"
}

if [[ -n "${PR_TITLE:-}" ]]; then
scan "PR title" <<<"$PR_TITLE"
fi
if [[ -n "${PR_BODY:-}" ]]; then
scan "PR body" <<<"$PR_BODY"
fi
if [[ -n "${PR_BODY_FILE:-}" ]]; then
scan "PR body" <"$PR_BODY_FILE"
fi

# One line per commit: sha, then the base64 message (messages span lines).
if [[ -n "${PR_NUMBER:-}" ]]; then
commits=$(gh api --paginate "repos/${GH_REPO}/pulls/${PR_NUMBER}/commits" \
--jq '.[] | "\(.sha) \(.commit.message | @base64)"')
else
base="${CHECK_BASE:-$(git merge-base HEAD origin/main)}"
commits=$(git rev-list "${base}..HEAD" | while read -r sha; do
echo "$sha $(git log -1 --format=%B "$sha" | base64 | tr -d "\n")"
done)
fi
while read -r sha encoded; do
[[ -z "$sha" ]] && continue
scan "commit ${sha:0:10}" < <(base64 -d <<<"$encoded")
done <<<"$commits"

if [[ "$found" -ne 0 ]]; then
echo "Pull request text must describe only the code change. Remove the details above"
echo "(edit the PR title or body; reword commits on your own branch) and describe any"
echo "reason in general terms. See the Privacy section of .agents/skills/git/SKILL.md."
exit 1
fi
echo "No personal or support-channel details found."
shell: bash
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ Real files live in `.agents/`; tool-specific directories only symlink to it.
explicit permission for that deploy.
- Never reset databases or wipe stack volumes without approval.
- Never paste or commit secrets.
- PR titles and bodies, commits, review replies, comments, and docs describe
only the code. Never name people, customers, their companies or products,
places, or support channels and tickets; state any reason in general terms.
See the `git` skill's Privacy section; CI's `PR Privacy Check` enforces it.
- Never `git stash`; it's shared across sessions.
- Memory is context, not authority; verify against code.
- Treat hosted dev data as real. `.cursor/*.sh` is Cursor Cloud only.
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ Keep PR bodies concise and write them yourself. A few sentences covering what
changed and why, a link to the issue, and anything a reviewer needs to know.
No generated boilerplate, no exhaustive file-by-file change lists.

Describe the code change only. Leave out names of people and customers,
contact details, @mentions, and references to support channels, tickets, or
conversations; state the reason in general terms. The `PR Privacy Check`
workflow fails PRs whose title, body, or commits contain contact details,
mentions, or support links; run `just pr-privacy` to check before pushing.

## Development setup

You do not need the local stack if you only change the frontend.
Expand Down
10 changes: 10 additions & 0 deletions tooling/just/check.just
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,13 @@ check *ARGS:
echo
if [ "$fail" -ne 0 ]; then echo "FAIL"; exit 1; fi
echo "OK"

# just pr-privacy commits since origin/main
# PR_TITLE="fix(x): ..." just pr-privacy body.md plus the PR title and body
#
# The same script CI's PR Privacy Check runs; the rule is the Privacy section
# of .agents/skills/git/SKILL.md.
# Scan branch commits (and a PR title and body) for personal or support details.
pr-privacy BODY="":
@cd {{ justfile_directory() }} && PR_BODY_FILE="{{ BODY }}" \
bash tooling/xtask/crates/xtask_workflows/src/workflows/scripts/pr_privacy_check.sh
6 changes: 6 additions & 0 deletions tooling/xtask/crates/xtask_workflows/src/workflows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ mod ios_release_drift;
mod ios_release_watch;
mod native_check_label;
mod path_validation;
mod pr_privacy_check;
mod publish_sdk;
mod pulumi_preview_pr;
mod push_local_stack_binaries;
Expand Down Expand Up @@ -285,6 +286,11 @@ const WORKFLOWS: &[WorkflowFile] = &[
file_name: "ios_preview.yml",
render_yaml: || render_gh_workflow(ios_preview::ios_preview)(),
},
WorkflowFile {
slug: "pr_privacy_check",
file_name: "pr-privacy-check.yml",
render_yaml: || render_gh_workflow(pr_privacy_check::pr_privacy_check)(),
},
WorkflowFile {
slug: "push_local_stack_binaries",
file_name: "push_local_stack_binaries.yml",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! `PR Privacy Check` — fails a PR whose title, body, or commit messages carry
//! personal or support-channel details (emails, phone numbers, @mentions,
//! chat-channel names, support-tool or Macro workspace links). The rule is the
//! Privacy section of `.agents/skills/git/SKILL.md`; `just pr-privacy` runs the
//! same script locally.
//!
//! It runs on `edited` too, so fixing the description re-runs the check. Title
//! and body reach the script through env vars, never interpolated into shell
//! code. There is no checkout: commits come from the PR API. Dependabot PRs are
//! skipped: their bodies quote upstream release notes, not our writing.

use gh_workflow::{
Event, Expression, Job, Level, Permissions, PullRequest, PullRequestType, Step, Workflow,
};

use crate::workflows::runners;

/// Build the workflow.
pub fn pr_privacy_check() -> Workflow {
Workflow::new("PR Privacy Check")
.on(Event::default().pull_request(
PullRequest::default()
.add_type(PullRequestType::Opened)
.add_type(PullRequestType::Edited)
.add_type(PullRequestType::Reopened)
.add_type(PullRequestType::Synchronize),
))
.add_job("check", check_job())
}

fn check_job() -> Job {
Job::default()
.name("PR text has no personal or support-channel details")
.cond(Expression::new(
"github.event.pull_request.user.login != 'dependabot[bot]'",
))
.runs_on(runners::Runner::TinyNoCache.to_string())
.timeout_minutes(5u32)
.permissions(
Permissions::default()
.contents(Level::Read)
.pull_requests(Level::Read),
)
.add_env(("GH_TOKEN", "${{ github.token }}"))
.add_env(("GH_REPO", "${{ github.repository }}"))
.add_env(("PR_NUMBER", "${{ github.event.pull_request.number }}"))
.add_env(("PR_TITLE", "${{ github.event.pull_request.title }}"))
.add_env(("PR_BODY", "${{ github.event.pull_request.body }}"))
.add_step(
Step::new("Scan PR title, body, and commit messages")
.run(include_str!("scripts/pr_privacy_check.sh"))
.shell("bash"),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Fail when a PR title, PR body, or commit message carries personal or
# support-channel details. The rule lives in the git skill
# (.agents/skills/git/SKILL.md, "Privacy"); this catches the patterns a
# regex can see: emails, phone numbers, @mentions, chat-channel names, and
# links into support tools or Macro workspace content. Names of people and
# customers can't be matched mechanically; reviewers own those.
#
# CI: PR_TITLE, PR_BODY, PR_NUMBER, GH_REPO, GH_TOKEN; commits come from the
# PR API. Locally (`just pr-privacy [body-file]`): commits are
# CHECK_BASE..HEAD (default: merge-base with origin/main); PR_TITLE and the
# optional body file are scanned when given.
set -euo pipefail

found=0

# report SOURCE LABEL MATCHES: print each match under its source.
report() {
local source="$1" label="$2" matches="$3"
[[ -z "$matches" ]] && return 0
found=1
while IFS= read -r m; do
echo "::error::${source}: ${label}: ${m}"
done <<<"$matches"
}

# scan SOURCE: read text on stdin and report every banned pattern.
scan() {
local source="$1" text prose
# Trailers (Co-authored-by, Signed-off-by) are git identity metadata.
text=$(grep -viE '^[A-Za-z-]+-by: ' || true)
[[ -z "$text" ]] && return 0
# Mentions and channel names are only checked outside code, where
# decorators, CSS colors, and shell comments look alike.
prose=$(awk '/^[[:space:]]*(```|~~~)/ { fenced = !fenced; next } !fenced' <<<"$text" | sed -E 's/`[^`]*`//g')

report "$source" "email address" "$(grep -oE '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' <<<"$text" \
| grep -viE '@(example\.(com|org|net)|([a-z0-9-]+\.)?noreply\.github\.com|(group|resource)\.calendar\.google\.com)$|^git@github\.com$' || true)"
report "$source" "phone number" "$(grep -oE '(\+[0-9]{1,3}[ .-]?)?\(?[0-9]{3}\)?[ .-][0-9]{3}[ .-][0-9]{4}' <<<"$text" || true)"
report "$source" "support or chat link" "$(grep -oiE 'https?://([a-z0-9-]+\.slack\.com/(archives|client|team)|app\.slack\.com/client|app\.intercom\.(com|io)|[a-z0-9-]+\.zendesk\.com/agent|app\.frontapp\.com|discord\.com/channels|mail\.google\.com|app\.hubspot\.com|app\.usepylon\.com|app\.plain\.com|app\.crisp\.chat|secure\.helpscout\.net)[^[:space:])>]*' <<<"$text" || true)"
# Links to workspace items (channels, documents, chats, emails). Tasks are
# the issue tracker and may be linked like GitHub issues.
report "$source" "Macro workspace link" "$(grep -oiE 'https?://([a-z0-9-]+\.)?macro\.com/app/[a-z-]+/[A-Za-z0-9-]+[^[:space:])>`]*' <<<"$text" \
| grep -viE '/app/tasks?/' || true)"
# `@scope/package` names end in a slash and are not mentions.
report "$source" "@mention" "$(grep -oE '(^|[[:space:](])@[A-Za-z0-9][A-Za-z0-9-]*/?' <<<"$prose" \
| sed -E 's/^[[:space:](]*//' | grep -v '/$' \
| grep -viE '^@(mentions?|coderabbitai|dependabot|github-actions|copilot|claude|cursor)$' || true)"
report "$source" "chat channel" "$(grep -oE '(^|[[:space:]])#[a-z][a-z0-9_-]*' <<<"$prose" \
| sed -E 's/^[[:space:]]*//' \
| grep -vE '^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$' || true)"
}

if [[ -n "${PR_TITLE:-}" ]]; then
scan "PR title" <<<"$PR_TITLE"
fi
if [[ -n "${PR_BODY:-}" ]]; then
scan "PR body" <<<"$PR_BODY"
fi
if [[ -n "${PR_BODY_FILE:-}" ]]; then
scan "PR body" <"$PR_BODY_FILE"
fi

# One line per commit: sha, then the base64 message (messages span lines).
if [[ -n "${PR_NUMBER:-}" ]]; then
commits=$(gh api --paginate "repos/${GH_REPO}/pulls/${PR_NUMBER}/commits" \
--jq '.[] | "\(.sha) \(.commit.message | @base64)"')
else
base="${CHECK_BASE:-$(git merge-base HEAD origin/main)}"
commits=$(git rev-list "${base}..HEAD" | while read -r sha; do
echo "$sha $(git log -1 --format=%B "$sha" | base64 | tr -d "\n")"
done)
fi
while read -r sha encoded; do
[[ -z "$sha" ]] && continue
scan "commit ${sha:0:10}" < <(base64 -d <<<"$encoded")
done <<<"$commits"

if [[ "$found" -ne 0 ]]; then
echo "Pull request text must describe only the code change. Remove the details above"
echo "(edit the PR title or body; reword commits on your own branch) and describe any"
echo "reason in general terms. See the Privacy section of .agents/skills/git/SKILL.md."
exit 1
fi
echo "No personal or support-channel details found."
Loading