Skip to content
Draft
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 .claude/skills
1 change: 1 addition & 0 deletions .gemini/skills
172 changes: 172 additions & 0 deletions .github/scripts/check_agent_leftovers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
#
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Fail a pull request that carries leftovers from local or agent-assisted work.

Only what the pull request adds is checked: added lines of the diff, the
names of added files, the messages of its commits, and its title and body.
Existing content on the base branch is never flagged.

Things that are fine on the author's machine but wrong in a public repo:

local paths file:// URLs into home or temp directories, and home
directories themselves (/Users/x/, /home/x/,
/usr/local/google/, C:\\Users\\x\\)
internal links go/ short links, b/ bug IDs, corp and googleplex hosts
secrets Google API keys
agent traces AI co-author trailers, links to agent chat sessions
local files agent settings meant to stay local, merge leftovers

A line that legitimately needs one of these can opt out by containing
"agent-leftovers: ignore".

Usage, from the repository root:

BASE_SHA=<base commit> [HEAD_SHA=<head, default HEAD>] [PR_TITLE=...] [PR_BODY=...] \\
check_agent_leftovers.py

Findings are printed as GitHub Actions error annotations.
"""

import os
import re
import subprocess
import sys

IGNORE_MARKER = "agent-leftovers: ignore"

# This script and its workflow describe the patterns, so they would match them.
SKIPPED_FILES = {
".github/scripts/check_agent_leftovers.py",
".github/workflows/agent-leftovers.yml",
}

CONTENT_PATTERNS = [
("local file URL", re.compile(r"file:///(Users|home|usr/local/google|private/var|var/folders|[A-Za-z]:)")),
("local path", re.compile(r"/usr/local/google/")),
("local path", re.compile(r"(?<![\w.])/Users/[\w.-]+/")),
("local path", re.compile(r"(?<![\w.])/home/(?!runner/)[\w.-]+/")),
("local path", re.compile(r"\b[A-Za-z]:\\+Users\\+[\w.-]+")),
("internal short link", re.compile(r"(?<![\w./-])go/[A-Za-z0-9][\w.-]*")),
("internal short link", re.compile(r"https?://go/")),
("internal bug ID", re.compile(r"(?<![\w./-])b/\d{6,}")),
("internal host", re.compile(r"\b[\w.-]+\.corp\.google\.com\b")),
("internal host", re.compile(r"\bgoogleplex\.com\b")),
("Google API key", re.compile(r"AIza[0-9A-Za-z_-]{35}")),
("agent session link", re.compile(r"claude\.ai/(code/)?(chat|session)")),
("agent session link", re.compile(r"chatgpt\.com/(c|share)/")),
("agent session link", re.compile(r"gemini\.google\.com/(app|share)/")),
("agent attribution", re.compile(r"Generated with \[?(Claude|Gemini|Copilot|Codex|Cursor)", re.I)),
(
"AI co-author trailer",
re.compile(r"Co-Authored-By:.*\b(Claude|Anthropic|Gemini|Copilot|Codex|Cursor|OpenAI)\b", re.I),
),
]

FILE_PATTERNS = [
("local agent settings", re.compile(r"(^|/)\.claude/settings\.local\.json$")),
("local agent memory", re.compile(r"(^|/)CLAUDE\.local\.md$")),
("merge leftover", re.compile(r"\.(orig|rej)$")),
]


def git(*args):
return subprocess.run(
["git", *args], check=True, capture_output=True, text=True
).stdout


def scan_text(text):
"""Yield the description of every pattern found in text, line by line."""
for line in text.splitlines():
if IGNORE_MARKER in line:
continue
for name, pattern in CONTENT_PATTERNS:
match = pattern.search(line)
if match:
yield f"{name}: {match.group(0)}"


def added_lines(base, head):
"""Yield (path, line number, text) for every line the diff adds."""
diff = git("diff", "--unified=0", "--no-color", "--no-ext-diff", f"{base}...{head}")
path = None
line_no = 0
for line in diff.splitlines():
if line.startswith("+++ "):
path = line[6:] if line.startswith("+++ b/") else None
elif line.startswith("@@"):
hunk = re.match(r"@@ -\S+ \+(\d+)", line)
line_no = int(hunk.group(1)) if hunk else 0
elif line.startswith("+") and path:
yield path, line_no, line[1:]
line_no += 1


def annotate(message, path=None, line=None):
location = ""
if path:
location = f" file={path}" + (f",line={line}" if line else "")
print(f"::error{location}::{message}")


def main():
base = os.environ.get("BASE_SHA")
head = os.environ.get("HEAD_SHA", "HEAD")
if not base:
print("BASE_SHA is not set.", file=sys.stderr)
return 2

findings = 0

for path, line_no, text in added_lines(base, head):
if path in SKIPPED_FILES:
continue
for finding in scan_text(text):
annotate(finding, path, line_no)
findings += 1

for path in git("diff", "--name-only", "--diff-filter=A", f"{base}...{head}").splitlines():
for name, pattern in FILE_PATTERNS:
if pattern.search(path):
annotate(f"{name}: {path} should not be committed", path)
findings += 1

for entry in git("log", "--no-merges", "--format=%h%x00%B%x1e", f"{base}..{head}").split("\x1e"):
if not entry.strip():
continue
sha, _, message = entry.strip().partition("\x00")
for finding in scan_text(message):
annotate(f"commit {sha} message has {finding}")
findings += 1

for field in ("PR_TITLE", "PR_BODY"):
for finding in scan_text(os.environ.get(field, "")):
annotate(f"pull request {field[3:].lower()} has {finding}")
findings += 1

if findings:
print(
f"\nFound {findings} leftover(s). Remove them, or add "
f'"{IGNORE_MARKER}" to a line that legitimately needs one.'
)
return 1
print("No agent leftovers found.")
return 0


if __name__ == "__main__":
sys.exit(main())
46 changes: 46 additions & 0 deletions .github/workflows/agent-leftovers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Fails a pull request that adds local paths, internal links, API keys or
# agent traces. See .github/scripts/check_agent_leftovers.py for the list.
name: Agent leftovers

on:
pull_request:
types: [opened, synchronize, reopened, edited]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout Repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Check for agent leftovers
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: python3 .github/scripts/check_agent_leftovers.py
4 changes: 2 additions & 2 deletions .github/workflows/release-rc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
sed -i 's/version = "[^"]*"/version = "'"$VERSION"'"/' build.gradle.kts

# Files with x-release-please-version inline marker
for file in llm-integration-prompt.md MIGRATION.md .gemini/skills/android-maps-utils/SKILL.md; do
for file in llm-integration-prompt.md MIGRATION.md .agents/skills/android-maps-utils/SKILL.md; do
if [ -f "$file" ]; then
sed -i "/x-release-please-version/s/[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[a-zA-Z0-9.-]*/$VERSION/" "$file"
fi
Expand All @@ -70,7 +70,7 @@ jobs:
build.gradle.kts \
llm-integration-prompt.md \
MIGRATION.md \
.gemini/skills/android-maps-utils/SKILL.md
.agents/skills/android-maps-utils/SKILL.md
git commit -m "chore: release v${VERSION}"
git push origin "$BRANCH"

Expand Down
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ Running the `demo` app requires a Maps API key in `secrets.properties` at the
repo root (see `local.defaults.properties` for the template). Never hardcode
or commit API keys.

## Agent setup

This file is the shared instruction set for every coding agent. `CLAUDE.md`
and `GEMINI.md` only import it, and Codex and Copilot read it directly, so put
new guidance here rather than in a tool-specific file.

Skills live in `.agents/skills/`. `.claude/skills` and `.gemini/skills` are
symlinks to that folder, so edit the files under `.agents/skills/` only.
`.agents/skills/android-maps-utils/SKILL.md` teaches agents how apps use this
library, and release-please bumps its version lines.

## Code style

- Match the language and idiom of the file you are editing; new code is
Expand Down Expand Up @@ -107,6 +118,18 @@ or commit API keys.

The PR title still has to be a valid Conventional Commit. Each line counts
for the version bump, so a `feat:` or `!` line bumps accordingly.
- Keep a pull request to what its title says. Once review has started, put
new features or unrelated fixes in a separate pull request. If the scope
does change, update the type in the title (a `refactor:` that now adds
behavior is a `feat:`), since release-please reads it.
- Answer every review thread before asking for another review: push the
change, or reply in the thread with why not. A new commit alone does not
tell the reviewer which comments it addresses.
- Never commit anything that only makes sense on your machine or inside
Google: local paths (`file:///`, `/Users/...`, workstation home
directories), `go/` links, `b/` bug IDs or API keys. The `Agent leftovers` check fails
the pull request on these, and on AI co-author trailers and links to agent
chat sessions.
- Every behavior change needs a unit test in the affected module.
- All pull requests are to be created as drafts (`gh pr create --draft`) until authorization is explicitly given to mark them ready for review. Always inform the user that the PR was created as a draft.
- Run the module's tests and `lint` before declaring work done, and report
Expand Down
1 change: 1 addition & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
2 changes: 1 addition & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"extra-files": [
"README.md",
"build.gradle.kts",
".gemini/skills/android-maps-utils/SKILL.md",
".agents/skills/android-maps-utils/SKILL.md",
"llm-integration-prompt.md",
"MIGRATION.md"
]
Expand Down
Loading