Compare commits

..

1 Commits

Author SHA1 Message Date
Claude
69585d4ffa feat: Add slack-quote-formatter plugin for better quoted message display
Adds a new plugin that transforms plain Slack forwarded message blocks into
visually distinctive formatted boxes using Unicode box-drawing characters.

Before:
  [Forwarded Message]
  From: Felix Klock
  Message: Here is some text...

After:
  ╔════════════════════════════════════════════════════════════════════╗
  ║  FORWARDED MESSAGE                                                 ║
  ╠════════════════════════════════════════════════════════════════════╣
  ║  From: Felix Klock                                                 ║
  ╟────────────────────────────────────────────────────────────────────╢
  ║    Here is some text...                                            ║
  ╚════════════════════════════════════════════════════════════════════╝

The plugin uses a UserPromptSubmit hook to intercept prompts containing
[Forwarded Message] blocks and reformats them for better visual distinction.

https://claude.ai/code/session_01YNRC6p2gAMSQZPX2NGK5F1
2026-02-10 19:46:10 +00:00
31 changed files with 727 additions and 2206 deletions

View File

@@ -1,5 +1,5 @@
---
allowed-tools: Bash(./scripts/gh.sh:*), Bash(./scripts/comment-on-duplicates.sh:*)
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(./scripts/comment-on-duplicates.sh:*)
description: Find duplicate GitHub issues
---
@@ -13,15 +13,11 @@ To do this, follow these steps precisely:
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, use the comment script to post duplicates:
```
./scripts/comment-on-duplicates.sh --potential-duplicates <dup1> <dup2> <dup3>
./scripts/comment-on-duplicates.sh --base-issue <issue-number> --potential-duplicates <dup1> <dup2> <dup3>
```
Notes (be sure to tell this to your agents, too):
- Use `./scripts/gh.sh` to interact with Github, rather than web fetch or raw `gh`. Examples:
- `./scripts/gh.sh issue view 123` — view an issue
- `./scripts/gh.sh issue view 123 --comments` — view with comments
- `./scripts/gh.sh issue list --state open --limit 20` — list issues
- `./scripts/gh.sh search issues "query" --limit 10` — search for issues
- Do not use other tools, beyond `./scripts/gh.sh` and the comment script (eg. don't use other MCP servers, file edit, etc.)
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` and the comment script (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first

View File

@@ -0,0 +1,40 @@
---
allowed-tools: Bash(gh issue list:*), Bash(gh issue view:*), Bash(gh issue edit:*), TodoWrite
description: Triage GitHub issues and label critical ones for oncall
---
You're an oncall triage assistant for GitHub issues. Your task is to identify critical issues that require immediate oncall attention and apply the "oncall" label.
Repository: anthropics/claude-code
Task overview:
1. First, get all open bugs updated in the last 3 days with at least 50 engagements:
```bash
gh issue list --repo anthropics/claude-code --state open --label bug --limit 1000 --json number,title,updatedAt,comments,reactions | jq -r '.[] | select((.updatedAt >= (now - 259200 | strftime("%Y-%m-%dT%H:%M:%SZ"))) and ((.comments | length) + ([.reactions[].content] | length) >= 50)) | "\(.number)"'
```
2. Save the list of issue numbers and create a TODO list with ALL of them. This ensures you process every single one.
3. For each issue in your TODO list:
- Use `gh issue view <number> --repo anthropics/claude-code --json title,body,labels,comments` to get full details
- Read and understand the full issue content and comments to determine actual user impact
- Evaluate: Is this truly blocking users from using Claude Code?
- Consider: "crash", "stuck", "frozen", "hang", "unresponsive", "cannot use", "blocked", "broken"
- Does it prevent core functionality? Can users work around it?
- Be conservative - only flag issues that truly prevent users from getting work done
4. For issues that are truly blocking and don't already have the "oncall" label:
- Use `gh issue edit <number> --repo anthropics/claude-code --add-label "oncall"`
- Mark the issue as complete in your TODO list
5. After processing all issues, provide a summary:
- List each issue number that received the "oncall" label
- Include the issue title and brief reason why it qualified
- If no issues qualified, state that clearly
Important:
- Process ALL issues in your TODO list systematically
- Don't post any comments to issues
- Only add the "oncall" label, never remove it
- Use individual `gh issue view` commands instead of bash for loops to avoid approval prompts

View File

@@ -1,74 +0,0 @@
---
allowed-tools: Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*)
description: Triage GitHub issues by analyzing and applying labels
---
You're an issue triage assistant. Analyze the issue and manage labels.
IMPORTANT: Don't post any comments or messages to the issue. Your only actions are adding or removing labels.
Context:
$ARGUMENTS
TOOLS:
- `./scripts/gh.sh` — wrapper for `gh` CLI. Only supports these subcommands and flags:
- `./scripts/gh.sh label list` — fetch all available labels
- `./scripts/gh.sh label list --limit 100` — fetch with limit
- `./scripts/gh.sh issue view 123` — read issue title, body, and labels
- `./scripts/gh.sh issue view 123 --comments` — read the conversation
- `./scripts/gh.sh issue list --state open --limit 20` — list issues
- `./scripts/gh.sh search issues "query"` — find similar or duplicate issues
- `./scripts/gh.sh search issues "query" --limit 10` — search with limit
- `./scripts/edit-issue-labels.sh --add-label LABEL --remove-label LABEL` — add or remove labels (issue number is read from the workflow event)
TASK:
1. Run `./scripts/gh.sh label list` to fetch the available labels. You may ONLY use labels from this list. Never invent new labels.
2. Run `./scripts/gh.sh issue view ISSUE_NUMBER` to read the issue details.
3. Run `./scripts/gh.sh issue view ISSUE_NUMBER --comments` to read the conversation.
**If EVENT is "issues" (new issue):**
4. First, check if this issue is actually about Claude Code.
- Look for Claude Code signals in the issue BODY: a `Claude Code Version` field or `claude --version` output, references to the `claude` CLI command, terminal sessions, the VS Code/JetBrains extensions, `CLAUDE.md` files, `.claude/` directories, MCP servers, Cowork, Remote Control, or the web UI at claude.ai/code. If ANY such signal is present, this IS a Claude Code issue — proceed to step 5.
- Only if NO Claude Code signals are present: check whether a different Anthropic product (claude.ai chat, Claude Desktop/Mobile apps, the raw Anthropic API/SDK, or account billing with no CLI involvement) is the *subject* of the complaint, not merely mentioned for context. If so, apply `invalid` and stop. If ambiguous, proceed to step 5 WITHOUT applying `invalid`.
- The body text is authoritative. If a form dropdown (e.g. Platform) contradicts evidence in the body, trust the body — dropdowns are often mis-selected.
5. Analyze and apply category labels:
- Type (bug, enhancement, question, etc.)
- Technical areas and platform
- Check for duplicates with `./scripts/gh.sh search issues`. Only mark as duplicate of OPEN issues.
6. Evaluate lifecycle labels:
- `needs-repro` (bugs only, 7 days): Bug reports without clear steps to reproduce. A good repro has specific, followable steps that someone else could use to see the same issue.
Do NOT apply if the user already provided error messages, logs, file paths, or a description of what they did. Don't require a specific format — narrative descriptions count.
For model behavior issues (e.g. "Claude does X when it should do Y"), don't require traditional repro steps — examples and patterns are sufficient.
- `needs-info` (bugs only, 7 days): The issue needs something from the community before it can progress — e.g. error messages, versions, environment details, or answers to follow-up questions. Don't apply to questions or enhancements.
Do NOT apply if the user already provided version, environment, and error details. If the issue just needs engineering investigation, that's not `needs-info`.
Issues with these labels are automatically closed after the timeout if there's no response.
The goal is to avoid issues lingering without a clear next step.
7. Apply all selected labels:
`./scripts/edit-issue-labels.sh --add-label "label1" --add-label "label2"`
**If EVENT is "issue_comment" (comment on existing issue):**
4. Evaluate lifecycle labels based on the full conversation:
- If the issue has `stale` or `autoclose`, remove the label — a new human comment means the issue is still active:
`./scripts/edit-issue-labels.sh --remove-label "stale" --remove-label "autoclose"`
- If the issue has `needs-repro` or `needs-info` and the missing information has now been provided, remove the label:
`./scripts/edit-issue-labels.sh --remove-label "needs-repro"`
- If the issue doesn't have lifecycle labels but clearly needs them (e.g., a maintainer asked for repro steps or more details), add the appropriate label.
- Comments like "+1", "me too", "same here", or emoji reactions are NOT the missing information. Only remove `needs-repro` or `needs-info` when substantive details are actually provided.
- Do NOT add or remove category labels (bug, enhancement, etc.) on comment events.
GUIDELINES:
- ONLY use labels from `./scripts/gh.sh label list` — never create or guess label names
- DO NOT post any comments to the issue
- Be conservative with lifecycle labels — only apply when clearly warranted
- Only apply lifecycle labels (`needs-repro`, `needs-info`) to bugs — never to questions or enhancements
- When in doubt, don't apply a lifecycle label — false positives are worse than missing labels
- On new issues (EVENT "issues"), always apply exactly one of `bug`, `enhancement`, `question`, `invalid`, or `duplicate`. If unsure, pick the closest fit — an imperfect category label is better than none.
- On comment events, it's okay to make no changes if nothing applies.

View File

@@ -17,6 +17,7 @@ jobs:
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
@@ -26,7 +27,6 @@ jobs:
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_CODE_SCRIPT_CAPS: '{"comment-on-duplicates.sh":1}'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"

View File

@@ -1,39 +1,105 @@
name: Claude Issue Triage
description: Automatically triage GitHub issues using Claude Code
on:
issues:
types: [opened]
issue_comment:
types: [created]
jobs:
triage-issue:
runs-on: ubuntu-latest
timeout-minutes: 10
if: >-
github.event_name == 'issues' ||
(github.event_name == 'issue_comment' && !github.event.issue.pull_request && github.event.comment.user.type != 'Bot')
concurrency:
group: issue-triage-${{ github.event.issue.number }}
cancel-in-progress: true
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-7aced2b"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
}
}
EOF
- name: Run Claude Code for Issue Triage
timeout-minutes: 5
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
prompt: "/triage-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }} EVENT: ${{ github.event_name }}"
prompt: |
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
Issue Information:
- REPO: ${{ github.repository }}
- ISSUE_NUMBER: ${{ github.event.issue.number }}
TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use the GitHub tools to get context about the issue:
- You have access to these tools:
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
- Start by using mcp__github__get_issue to get the issue details
3. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels:
- Use mcp__github__update_issue to apply your selected labels
- DO NOT post any comments explaining your decision
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
IMPORTANT GUIDELINES:
- Be thorough in your analysis
- Only select labels from the provided list above
- DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--model claude-opus-4-6
--model claude-sonnet-4-5-20250929
--mcp-config /tmp/mcp-config/mcp-servers.json
--allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"

View File

@@ -1,27 +0,0 @@
name: "Issue Lifecycle Comment"
on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Post lifecycle comment
run: bun run scripts/lifecycle-comment.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LABEL: ${{ github.event.label.name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}

View File

@@ -1,47 +0,0 @@
name: Non-write Users Check
on:
pull_request:
paths:
- ".github/**"
permissions:
contents: read
pull-requests: write
jobs:
allowed-non-write-check:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- run: |
DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" || true)
if ! echo "$DIFF" | grep -qE '^diff --git a/\.github/.*\.ya?ml'; then
exit 0
fi
MATCHES=$(echo "$DIFF" | grep "^+.*allowed_non_write_users" || true)
if [ -z "$MATCHES" ]; then
exit 0
fi
EXISTING=$(gh pr view "$PR_NUMBER" -R "$REPO" --json comments --jq '.comments[].body' \
| grep -c "<!-- non-write-users-check -->" || true)
if [ "$EXISTING" -gt 0 ]; then
exit 0
fi
gh pr comment "$PR_NUMBER" -R "$REPO" --body '<!-- non-write-users-check -->
**`allowed_non_write_users` detected**
This PR adds or modifies `allowed_non_write_users`, which allows users without write access to trigger Claude Code Action workflows. This can introduce security risks.
If this is a new flow, please make sure you actually need `allowed_non_write_users`. If you are editing an existing workflow, double check that you are not adding new Claude permissions which might lead to a vulnerability.
See existing workflows in this repo for safe usage examples, or contact the AppSec team.'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}

118
.github/workflows/oncall-triage.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Oncall Issue Triage
description: Automatically identify and label critical blocking issues requiring oncall attention
on:
push:
branches:
- add-oncall-triage-workflow # Temporary: for testing only
schedule:
# Run every 6 hours
- cron: '0 */6 * * *'
workflow_dispatch: # Allow manual trigger
jobs:
oncall-triage:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-7aced2b"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
}
}
EOF
- name: Run Claude Code for Oncall Triage
timeout-minutes: 10
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
prompt: |
You're an oncall triage assistant for GitHub issues. Your task is to identify critical issues that require immediate oncall attention.
Important: Don't post any comments or messages to the issues. Your only action should be to apply the "oncall" label to qualifying issues.
Repository: ${{ github.repository }}
Task overview:
1. Fetch all open issues updated in the last 3 days:
- Use mcp__github__list_issues with:
- state="open"
- first=5 (fetch only 5 issues per page)
- orderBy="UPDATED_AT"
- direction="DESC"
- This will give you the most recently updated issues first
- For each page of results, check the updatedAt timestamp of each issue
- Add issues updated within the last 3 days (72 hours) to your TODO list as you go
- Keep paginating using the 'after' parameter until you encounter issues older than 3 days
- Once you hit issues older than 3 days, you can stop fetching (no need to fetch all open issues)
2. Build your TODO list incrementally as you fetch:
- As you fetch each page, immediately add qualifying issues to your TODO list
- One TODO item per issue number (e.g., "Evaluate issue #123")
- This allows you to start processing while still fetching more pages
3. For each issue in your TODO list:
- Use mcp__github__get_issue to read the issue details (title, body, labels)
- Use mcp__github__get_issue_comments to read all comments
- Evaluate whether this issue needs the oncall label:
a) Is it a bug? (has "bug" label or describes bug behavior)
b) Does it have at least 50 engagements? (count comments + reactions)
c) Is it truly blocking? Read and understand the full content to determine:
- Does this prevent core functionality from working?
- Can users work around it?
- Consider severity indicators: "crash", "stuck", "frozen", "hang", "unresponsive", "cannot use", "blocked", "broken"
- Be conservative - only flag issues that truly prevent users from getting work done
4. For issues that meet all criteria and do not already have the "oncall" label:
- Use mcp__github__update_issue to add the "oncall" label
- Do not post any comments
- Do not remove any existing labels
- Do not remove the "oncall" label from issues that already have it
Important guidelines:
- Use the TODO list to track your progress through ALL candidate issues
- Process issues efficiently - don't read every single issue upfront, work through your TODO list systematically
- Be conservative in your assessment - only flag truly critical blocking issues
- Do not post any comments to issues
- Your only action should be to add the "oncall" label using mcp__github__update_issue
- Mark each issue as complete in your TODO list as you process it
7. After processing all issues in your TODO list, provide a summary of your actions:
- Total number of issues processed (candidate issues evaluated)
- Number of issues that received the "oncall" label
- For each issue that got the label: list issue number, title, and brief reason why it qualified
- Close calls: List any issues that almost qualified but didn't quite meet the criteria (e.g., borderline blocking, had workarounds)
- If no issues qualified, state that clearly
- Format the summary clearly for easy reading
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--mcp-config /tmp/mcp-config/mcp-servers.json
--allowedTools "mcp__github__list_issues,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue"

View File

@@ -0,0 +1,157 @@
name: "Manage Stale Issues"
on:
schedule:
# 2am Pacific = 9am UTC (10am UTC during DST)
- cron: "0 10 * * *"
workflow_dispatch:
permissions:
issues: write
concurrency:
group: stale-issue-manager
jobs:
manage-stale-issues:
runs-on: ubuntu-latest
steps:
- name: Manage stale issues
uses: actions/github-script@v7
with:
script: |
const oneMonthAgo = new Date();
oneMonthAgo.setDate(oneMonthAgo.getDate() - 30);
const twoMonthsAgo = new Date();
twoMonthsAgo.setDate(twoMonthsAgo.getDate() - 60);
const warningComment = `This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.`;
const closingComment = `This issue has been automatically closed due to 60 days of inactivity. If you're still experiencing this issue, please open a new issue with updated information.`;
let page = 1;
let hasMore = true;
let totalWarned = 0;
let totalClosed = 0;
let totalLabeled = 0;
while (hasMore) {
// Get open issues sorted by last updated (oldest first)
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc',
per_page: 100,
page: page
});
if (issues.length === 0) {
hasMore = false;
break;
}
for (const issue of issues) {
// Skip if already locked
if (issue.locked) continue;
// Skip pull requests
if (issue.pull_request) continue;
// Check if updated more recently than 30 days ago
const updatedAt = new Date(issue.updated_at);
if (updatedAt > oneMonthAgo) {
// Since issues are sorted by updated_at ascending,
// once we hit a recent issue, all remaining will be recent too
hasMore = false;
break;
}
// Check if issue has autoclose label
const hasAutocloseLabel = issue.labels.some(label =>
typeof label === 'object' && label.name === 'autoclose'
);
try {
// Get comments to check for existing warning
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
// Find the last comment from github-actions bot
const botComments = comments.filter(comment =>
comment.user && comment.user.login === 'github-actions[bot]' &&
comment.body && comment.body.includes('inactive for 30 days')
);
const lastBotComment = botComments[botComments.length - 1];
if (lastBotComment) {
// Check if the bot comment is older than 30 days (total 60 days of inactivity)
const botCommentDate = new Date(lastBotComment.created_at);
if (botCommentDate < oneMonthAgo) {
// Close the issue - it's been stale for 60+ days
console.log(`Closing issue #${issue.number} (stale for 60+ days): ${issue.title}`);
// Post closing comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: closingComment
});
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned'
});
totalClosed++;
}
// If bot comment exists but is recent, issue already has warning
} else if (updatedAt < oneMonthAgo) {
// No bot warning yet, issue is 30+ days old
console.log(`Warning issue #${issue.number} (stale for 30+ days): ${issue.title}`);
// Post warning comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: warningComment
});
totalWarned++;
// Add autoclose label if not present
if (!hasAutocloseLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['autoclose']
});
totalLabeled++;
}
}
} catch (error) {
console.error(`Failed to process issue #${issue.number}: ${error.message}`);
}
}
page++;
}
console.log(`Summary:`);
console.log(`- Issues warned (30 days stale): ${totalWarned}`);
console.log(`- Issues labeled with autoclose: ${totalLabeled}`);
console.log(`- Issues closed (60 days stale): ${totalClosed}`);

View File

@@ -1,31 +0,0 @@
name: "Issue Sweep"
on:
schedule:
- cron: "0 10,22 * * *"
workflow_dispatch:
permissions:
issues: write
concurrency:
group: daily-issue-sweep
jobs:
sweep:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Enforce lifecycle timeouts
run: bun run scripts/sweep.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
# MDM Deployment Examples
Example templates for deploying Claude Code [managed settings](https://code.claude.com/docs/en/settings#settings-files) through Jamf, Iru (Kandji), Intune, or Group Policy. Use these as starting points — adjust them to fit your needs.
All templates encode the same minimal example (`permissions.disableBypassPermissionsMode`). See the [settings reference](https://code.claude.com/docs/en/settings#available-settings) for the full list of keys, and [`../settings`](../settings) for more complete example configurations.
## Templates
> [!WARNING]
> These examples are community-maintained templates which may be unsupported or incorrect. You are responsible for the correctness of your own deployment configuration.
| File | Use with |
| :--- | :--- |
| [`managed-settings.json`](./managed-settings.json) | Any platform. Deploy to the [system config directory](https://code.claude.com/docs/en/settings#settings-files). |
| [`macos/com.anthropic.claudecode.plist`](./macos/com.anthropic.claudecode.plist) | Jamf or Iru (Kandji) **Custom Settings** payload. Preference domain: `com.anthropic.claudecode`. |
| [`macos/com.anthropic.claudecode.mobileconfig`](./macos/com.anthropic.claudecode.mobileconfig) | Full configuration profile for local testing or MDMs that take a complete profile. |
| [`windows/Set-ClaudeCodePolicy.ps1`](./windows/Set-ClaudeCodePolicy.ps1) | Intune **Platform scripts**. Writes `managed-settings.json` to `C:\Program Files\ClaudeCode\`. |
| [`windows/ClaudeCode.admx`](./windows/ClaudeCode.admx) + [`en-US/ClaudeCode.adml`](./windows/en-US/ClaudeCode.adml) | Group Policy or Intune **Import ADMX**. Writes `HKLM\SOFTWARE\Policies\ClaudeCode\Settings` (REG_SZ, single-line JSON). |
## Tips
- Replace the placeholder `PayloadUUID` and `PayloadOrganization` values in the `.mobileconfig` with your own (`uuidgen`)
- Before deploying to your fleet, test on a single machine and confirm `/status` lists the source under **Setting sources** — e.g. `Enterprise managed settings (plist)` on macOS or `Enterprise managed settings (HKLM)` on Windows
- Settings deployed this way sit at the top of the precedence order and cannot be overridden by users
## Full Documentation
See https://code.claude.com/docs/en/settings#settings-files for complete documentation on managed settings and settings precedence.

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadDisplayName</key>
<string>Claude Code Managed Settings</string>
<key>PayloadDescription</key>
<string>Configures managed settings for Claude Code.</string>
<key>PayloadIdentifier</key>
<string>com.anthropic.claudecode.profile</string>
<key>PayloadOrganization</key>
<string>Example Organization</string>
<key>PayloadScope</key>
<string>System</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>DC3CBC17-3330-4CDE-94AC-D2342E9C88A3</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadDisplayName</key>
<string>Claude Code</string>
<key>PayloadIdentifier</key>
<string>com.anthropic.claudecode.profile.BEFD5F54-71FC-4012-82B2-94399A1E220B</string>
<key>PayloadType</key>
<string>com.apple.ManagedClient.preferences</string>
<key>PayloadUUID</key>
<string>BEFD5F54-71FC-4012-82B2-94399A1E220B</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadContent</key>
<dict>
<key>com.anthropic.claudecode</key>
<dict>
<key>Forced</key>
<array>
<dict>
<key>mcx_preference_settings</key>
<dict>
<key>permissions</key>
<dict>
<key>disableBypassPermissionsMode</key>
<string>disable</string>
</dict>
</dict>
</dict>
</array>
</dict>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>permissions</key>
<dict>
<key>disableBypassPermissionsMode</key>
<string>disable</string>
</dict>
</dict>
</plist>

View File

@@ -1,5 +0,0 @@
{
"permissions": {
"disableBypassPermissionsMode": "disable"
}
}

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions"
revision="1.0" schemaVersion="1.0">
<policyNamespaces>
<target prefix="claudecode" namespace="Anthropic.Policies.ClaudeCode" />
<using prefix="windows" namespace="Microsoft.Policies.Windows" />
</policyNamespaces>
<resources minRequiredRevision="1.0" />
<categories>
<category name="Cat_ClaudeCode" displayName="$(string.Cat_ClaudeCode)" />
</categories>
<policies>
<policy name="ManagedSettings"
class="Machine"
displayName="$(string.ManagedSettings)"
explainText="$(string.ManagedSettings_Explain)"
presentation="$(presentation.ManagedSettings)"
key="SOFTWARE\Policies\ClaudeCode">
<parentCategory ref="Cat_ClaudeCode" />
<supportedOn ref="windows:SUPPORTED_Windows_10_0" />
<elements>
<text id="SettingsJson" valueName="Settings" maxLength="1000000" required="true" />
</elements>
</policy>
</policies>
</policyDefinitions>

View File

@@ -1,28 +0,0 @@
<#
Deploys Claude Code managed settings as a JSON file.
Intune: Devices > Scripts and remediations > Platform scripts > Add (Windows 10 and later).
Run this script using the logged on credentials: No
Run script in 64 bit PowerShell Host: Yes
Claude Code reads C:\Program Files\ClaudeCode\managed-settings.json at startup
and treats it as a managed policy source. Edit the JSON below to change the
deployed settings; see https://code.claude.com/docs/en/settings for available keys.
#>
$ErrorActionPreference = 'Stop'
$dir = Join-Path $env:ProgramFiles 'ClaudeCode'
New-Item -ItemType Directory -Path $dir -Force | Out-Null
$json = @'
{
"permissions": {
"disableBypassPermissionsMode": "disable"
}
}
'@
$path = Join-Path $dir 'managed-settings.json'
[System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false)))
Write-Output "Wrote $path"

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions"
revision="1.0" schemaVersion="1.0">
<displayName>Claude Code</displayName>
<description>Claude Code policy settings</description>
<resources>
<stringTable>
<string id="Cat_ClaudeCode">Claude Code</string>
<string id="ManagedSettings">Managed settings (JSON)</string>
<string id="ManagedSettings_Explain">Configures managed settings for Claude Code.
Enter the full settings configuration as a single line of JSON. The value is stored as a REG_SZ string at HKLM\SOFTWARE\Policies\ClaudeCode\Settings and is applied at the highest precedence; users cannot override these settings.
Example:
{"permissions":{"disableBypassPermissionsMode":"disable"}}
For the list of available settings keys, see https://code.claude.com/docs/en/settings.
If your configuration is large or you prefer to manage a JSON file directly, deploy C:\Program Files\ClaudeCode\managed-settings.json instead (see Set-ClaudeCodePolicy.ps1).</string>
</stringTable>
<presentationTable>
<presentation id="ManagedSettings">
<textBox refId="SettingsJson">
<label>Settings JSON:</label>
</textBox>
</presentation>
</presentationTable>
</resources>
</policyDefinitionResources>

View File

@@ -1,6 +1,6 @@
# Settings Examples
Example Claude Code settings files, primarily intended for organization-wide deployments. Use these as starting points — adjust them to fit your needs.
Example Claude Code settings files, primarily intended for organization-wide deployments. Use these are starting points — adjust them to fit your needs.
These may be applied at any level of the [settings hierarchy](https://code.claude.com/docs/en/settings#settings-files), though certain properties only take effect if specified in enterprise settings (e.g. `strictKnownMarketplaces`, `allowManagedHooksOnly`, `allowManagedPermissionRulesOnly`).
@@ -26,10 +26,6 @@ These may be applied at any level of the [settings hierarchy](https://code.claud
- Before deploying configuration files to your organization, test them locally by applying to `managed-settings.json`, `settings.json` or `settings.local.json`
- The `sandbox` property only applies to the `Bash` tool; it does not apply to other tools (like Read, Write, WebSearch, WebFetch, MCPs), hooks, or internal commands
## Deploying via MDM
To distribute these settings as enterprise-managed policy through Jamf, Iru (Kandji), Intune, or Group Policy, see the deployment templates in [`../mdm`](../mdm).
## Full Documentation
See https://code.claude.com/docs/en/settings for complete documentation on all available managed settings.

View File

@@ -25,6 +25,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
| [pr-review-toolkit](./pr-review-toolkit/) | Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification | **Command:** `/pr-review-toolkit:review-pr` - Run with optional review aspects (comments, tests, errors, types, code, simplify, all)<br>**Agents:** `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`, `code-reviewer`, `code-simplifier` |
| [ralph-wiggum](./ralph-wiggum/) | Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly until completion | **Commands:** `/ralph-loop`, `/cancel-ralph` - Start/stop autonomous iteration loops<br>**Hook:** Stop - Intercepts exit attempts to continue iteration |
| [security-guidance](./security-guidance/) | Security reminder hook that warns about potential security issues when editing files | **Hook:** PreToolUse - Monitors 9 security patterns including command injection, XSS, eval usage, dangerous HTML, pickle deserialization, and os.system calls |
| [slack-quote-formatter](./slack-quote-formatter/) | Enhances visual display of Slack forwarded and quoted messages with distinctive Unicode box formatting | **Hook:** UserPromptSubmit - Transforms `[Forwarded Message]` blocks into visually distinctive formatted boxes with clear boundaries and indentation |
## Installation

View File

@@ -56,19 +56,14 @@ Note: Still review Claude generated PR's.
6. Filter out any issues that were not validated in step 5. This step will give us our list of high signal issues for our review.
7. Output a summary of the review findings to the terminal:
- If issues were found, list each issue with a brief description.
- If no issues were found, state: "No issues found. Checked for bugs and CLAUDE.md compliance."
7. If issues were found, skip to step 8 to post inline comments directly.
If `--comment` argument was NOT provided, stop here. Do not post any GitHub comments.
If `--comment` argument IS provided and NO issues were found, post a summary comment using `gh pr comment` and stop.
If `--comment` argument IS provided and issues were found, continue to step 8.
If NO issues were found, post a summary comment using `gh pr comment` (if `--comment` argument is provided):
"No issues found. Checked for bugs and CLAUDE.md compliance."
8. Create a list of all comments that you plan on leaving. This is only for you to make sure you are comfortable with the comments. Do not post this list anywhere.
9. Post inline comments for each issue using `mcp__github_inline_comment__create_inline_comment` with `confirmed: true`. For each comment:
9. Post inline comments for each issue using `mcp__github_inline_comment__create_inline_comment`. For each comment:
- Provide a brief description of the issue
- For small, self-contained fixes, include a committable suggestion block
- For larger fixes (6+ lines, structural changes, or changes spanning multiple locations), describe the issue and suggested fix without a suggestion block
@@ -90,7 +85,7 @@ Notes:
- Use gh CLI to interact with GitHub (e.g., fetch pull requests, create comments). Do not use web fetch.
- Create a todo list before starting.
- You must cite and link each issue in inline comments (e.g., if referring to a CLAUDE.md, include a link to it).
- If no issues are found and `--comment` argument is provided, post a comment with the following format:
- If no issues are found, post a comment with the following format:
---

View File

@@ -0,0 +1,18 @@
{
"name": "slack-quote-formatter",
"version": "1.0.0",
"description": "Enhances visual display of Slack forwarded and quoted messages with distinctive formatting",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"
},
"license": "MIT",
"keywords": [
"slack",
"formatting",
"quotes",
"messages",
"visual"
],
"hooks": "./hooks/hooks.json"
}

View File

@@ -0,0 +1,87 @@
# Slack Quote Formatter
Enhances the visual display of Slack forwarded and quoted messages with distinctive Unicode box formatting.
## Overview
When Claude Code receives messages from Slack that include forwarded messages, they typically appear as plain text:
```
[Forwarded Message]
From: Felix Klock
Message: I assume it would be a matter of reviewing the entries...
```
This plugin transforms these plain blocks into visually distinctive formatted quotes:
```
╔════════════════════════════════════════════════════════════════════╗
║ FORWARDED MESSAGE ║
╠════════════════════════════════════════════════════════════════════╣
║ From: Felix Klock ║
╟────────────────────────────────────────────────────────────────────╢
║ I assume it would be a matter of reviewing the entries... ║
╚════════════════════════════════════════════════════════════════════╝
```
## Features
- Unicode box drawing characters for clear visual boundaries
- Distinct header section highlighting "FORWARDED MESSAGE"
- Sender information prominently displayed
- Message content indented for quote-like appearance
- Automatic text wrapping for long messages
- Handles multiple forwarded messages in a single prompt
## Installation
This plugin is included in the Claude Code plugins repository. To enable it:
1. Add the plugin to your project's `.claude/settings.json`:
```json
{
"plugins": [
"path/to/plugins/slack-quote-formatter"
]
}
```
2. Or install via the Claude Code plugin command:
```
/plugin add slack-quote-formatter
```
## How It Works
The plugin registers a `UserPromptSubmit` hook that:
1. Intercepts incoming prompts
2. Detects `[Forwarded Message]` blocks in the Slack context
3. Transforms them into formatted boxes with visual distinction
4. Passes the transformed prompt to Claude
## Configuration
No configuration required. The plugin automatically activates when Slack forwarded messages are detected.
## Plugin Structure
```
slack-quote-formatter/
├── .claude-plugin/
│ └── plugin.json # Plugin metadata
├── hooks/
│ ├── hooks.json # Hook configuration
│ └── format_slack_quotes.py # Transformation logic
└── README.md
```
## Why Visual Distinction Matters
When working with Slack context in Claude Code, quoted messages can easily blend into the surrounding text, making it harder to:
- Identify what content is quoted vs. original
- Understand the source of different pieces of information
- Parse complex threads with multiple forwarded messages
This plugin makes quoted content immediately recognizable, improving readability and reducing confusion.

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Slack Quote Formatter - UserPromptSubmit Hook
Transforms plain Slack forwarded/quoted messages into visually distinctive
formatted blocks with Unicode box characters and better layout.
Before:
[Forwarded Message]
From: John Doe
Message: Here is some text...
After:
╔═══════════════════════════════════════════════════════════╗
║ FORWARDED MESSAGE ║
╠═══════════════════════════════════════════════════════════╣
║ From: John Doe ║
╟───────────────────────────────────────────────────────────╢
║ Here is some text... ║
╚═══════════════════════════════════════════════════════════╝
"""
import json
import re
import sys
def format_forwarded_message(from_line: str, message_content: str) -> str:
"""Format a single forwarded message with visual box styling."""
# Box drawing characters
TOP_LEFT = ""
TOP_RIGHT = ""
BOTTOM_LEFT = ""
BOTTOM_RIGHT = ""
HORIZONTAL = ""
VERTICAL = ""
LEFT_T = ""
RIGHT_T = ""
LEFT_LIGHT = ""
RIGHT_LIGHT = ""
LIGHT_HORIZONTAL = ""
# Configuration
BOX_WIDTH = 70
INNER_WIDTH = BOX_WIDTH - 4 # Account for "║ " and " ║"
def pad_line(text: str, width: int = INNER_WIDTH) -> str:
"""Pad a line to fill the box width."""
if len(text) > width:
return text[:width]
return text + " " * (width - len(text))
def wrap_text(text: str, width: int) -> list:
"""Wrap text to fit within the specified width."""
words = text.split()
lines = []
current_line = []
current_length = 0
for word in words:
word_len = len(word)
if current_length + word_len + (1 if current_line else 0) <= width:
current_line.append(word)
current_length += word_len + (1 if len(current_line) > 1 else 0)
else:
if current_line:
lines.append(" ".join(current_line))
current_line = [word]
current_length = word_len
if current_line:
lines.append(" ".join(current_line))
return lines if lines else [""]
lines = []
# Top border with header
lines.append(f"{TOP_LEFT}{HORIZONTAL * (BOX_WIDTH - 2)}{TOP_RIGHT}")
# Header line
header = "FORWARDED MESSAGE"
lines.append(f"{VERTICAL} {pad_line(header)}{VERTICAL}")
# Separator after header
lines.append(f"{LEFT_T}{HORIZONTAL * (BOX_WIDTH - 2)}{RIGHT_T}")
# From line
from_display = from_line.strip()
lines.append(f"{VERTICAL} {pad_line(from_display)}{VERTICAL}")
# Light separator before message
lines.append(f"{LEFT_LIGHT}{LIGHT_HORIZONTAL * (BOX_WIDTH - 2)}{RIGHT_LIGHT}")
# Message content (indented and wrapped)
message_lines = message_content.strip().split("\n")
for msg_line in message_lines:
# Handle each line, wrapping if necessary
wrapped = wrap_text(msg_line, INNER_WIDTH - 2) # Extra indent
for wrapped_line in wrapped:
indented = " " + wrapped_line # Add indent for quote appearance
lines.append(f"{VERTICAL} {pad_line(indented)}{VERTICAL}")
# Bottom border
lines.append(f"{BOTTOM_LEFT}{HORIZONTAL * (BOX_WIDTH - 2)}{BOTTOM_RIGHT}")
return "\n".join(lines)
def transform_slack_context(text: str) -> str:
"""
Find and transform [Forwarded Message] blocks in the text.
Matches patterns like:
[Forwarded Message]
From: Name Here
Message: Content here that may span
multiple lines until we hit the next section
"""
# Pattern to match forwarded message blocks
# This handles the common format from Slack context
pattern = r'\[Forwarded Message\]\s*\n\s*From:\s*([^\n]+)\s*\nMessage:\s*(.+?)(?=\n\s*\[|\n\[pnkfelix\]|\n<|\Z)'
def replace_match(match):
from_name = match.group(1).strip()
message = match.group(2).strip()
formatted = format_forwarded_message(f"From: {from_name}", message)
return f"\n{formatted}\n"
# Apply transformation
result = re.sub(pattern, replace_match, text, flags=re.DOTALL)
return result
def main():
"""Main entry point for the UserPromptSubmit hook."""
try:
# Read input from stdin
input_data = json.load(sys.stdin)
# Get the user prompt if available
user_prompt = input_data.get("user_prompt", "")
if not user_prompt:
# No prompt to transform, just pass through
print(json.dumps({}))
sys.exit(0)
# Check if there are forwarded messages to format
if "[Forwarded Message]" not in user_prompt:
# Nothing to transform
print(json.dumps({}))
sys.exit(0)
# Transform the prompt
transformed_prompt = transform_slack_context(user_prompt)
# If we made changes, return the transformed prompt
if transformed_prompt != user_prompt:
result = {
"transformedPrompt": transformed_prompt
}
print(json.dumps(result))
else:
print(json.dumps({}))
except json.JSONDecodeError:
# No valid JSON input, pass through
print(json.dumps({}))
except Exception as e:
# Log error but don't block
error_output = {
"systemMessage": f"[slack-quote-formatter] Warning: {str(e)}"
}
print(json.dumps(error_output))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,14 @@
{
"UserPromptSubmit": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/format_slack_quotes.py",
"timeout": 5000
}
]
}
]
}

View File

@@ -1,28 +1,22 @@
#!/usr/bin/env bash
#
# Comments on a GitHub issue with a list of potential duplicates.
# Usage: ./comment-on-duplicates.sh --potential-duplicates 456 789 101
#
# The base issue number is read from the workflow event payload.
# Usage: ./comment-on-duplicates.sh --base-issue 123 --potential-duplicates 456 789 101
#
set -euo pipefail
REPO="anthropics/claude-code"
# Read from event payload so the issue number is bound to the triggering event.
# Falls back to workflow_dispatch inputs for manual runs.
BASE_ISSUE=$(jq -r '.issue.number // .inputs.issue_number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
if ! [[ "$BASE_ISSUE" =~ ^[0-9]+$ ]]; then
echo "Error: no issue number in event payload" >&2
exit 1
fi
BASE_ISSUE=""
DUPLICATES=()
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--base-issue)
BASE_ISSUE="$2"
shift 2
;;
--potential-duplicates)
shift
while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
@@ -31,12 +25,23 @@ while [[ $# -gt 0 ]]; do
done
;;
*)
echo "Error: unknown argument (only --potential-duplicates is accepted)" >&2
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
# Validate base issue
if [[ -z "$BASE_ISSUE" ]]; then
echo "Error: --base-issue is required" >&2
exit 1
fi
if ! [[ "$BASE_ISSUE" =~ ^[0-9]+$ ]]; then
echo "Error: --base-issue must be a number, got: $BASE_ISSUE" >&2
exit 1
fi
# Validate duplicates
if [[ ${#DUPLICATES[@]} -eq 0 ]]; then
echo "Error: --potential-duplicates requires at least one issue number" >&2

View File

@@ -1,84 +0,0 @@
#!/usr/bin/env bash
#
# Edits labels on a GitHub issue.
# Usage: ./edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged
#
# The issue number is read from the workflow event payload.
#
set -euo pipefail
# Read from event payload so the issue number is bound to the triggering event.
# Falls back to workflow_dispatch inputs for manual runs.
ISSUE=$(jq -r '.issue.number // .inputs.issue_number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
echo "Error: no issue number in event payload" >&2
exit 1
fi
ADD_LABELS=()
REMOVE_LABELS=()
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--add-label)
ADD_LABELS+=("$2")
shift 2
;;
--remove-label)
REMOVE_LABELS+=("$2")
shift 2
;;
*)
echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
exit 1
;;
esac
done
if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
exit 1
fi
# Fetch valid labels from the repo
VALID_LABELS=$(gh label list --limit 500 --json name --jq '.[].name')
# Filter to only labels that exist in the repo
FILTERED_ADD=()
for label in "${ADD_LABELS[@]}"; do
if echo "$VALID_LABELS" | grep -qxF "$label"; then
FILTERED_ADD+=("$label")
fi
done
FILTERED_REMOVE=()
for label in "${REMOVE_LABELS[@]}"; do
if echo "$VALID_LABELS" | grep -qxF "$label"; then
FILTERED_REMOVE+=("$label")
fi
done
if [[ ${#FILTERED_ADD[@]} -eq 0 && ${#FILTERED_REMOVE[@]} -eq 0 ]]; then
exit 0
fi
# Build gh command arguments
GH_ARGS=("issue" "edit" "$ISSUE")
for label in "${FILTERED_ADD[@]}"; do
GH_ARGS+=("--add-label" "$label")
done
for label in "${FILTERED_REMOVE[@]}"; do
GH_ARGS+=("--remove-label" "$label")
done
gh "${GH_ARGS[@]}"
if [[ ${#FILTERED_ADD[@]} -gt 0 ]]; then
echo "Added: ${FILTERED_ADD[*]}"
fi
if [[ ${#FILTERED_REMOVE[@]} -gt 0 ]]; then
echo "Removed: ${FILTERED_REMOVE[*]}"
fi

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Wrapper around gh CLI that only allows specific subcommands and flags.
# All commands are scoped to the current repository via GH_REPO or GITHUB_REPOSITORY.
#
# Usage:
# ./scripts/gh.sh issue view 123
# ./scripts/gh.sh issue view 123 --comments
# ./scripts/gh.sh issue list --state open --limit 20
# ./scripts/gh.sh search issues "search query" --limit 10
# ./scripts/gh.sh label list --limit 100
export GH_HOST=github.com
REPO="${GH_REPO:-${GITHUB_REPOSITORY:-}}"
if [[ -z "$REPO" || "$REPO" == */*/* || "$REPO" != */* ]]; then
echo "Error: GH_REPO or GITHUB_REPOSITORY must be set to owner/repo format (e.g., GITHUB_REPOSITORY=anthropics/claude-code)" >&2
exit 1
fi
export GH_REPO="$REPO"
ALLOWED_FLAGS=(--comments --state --limit --label)
FLAGS_WITH_VALUES=(--state --limit --label)
SUB1="${1:-}"
SUB2="${2:-}"
CMD="$SUB1 $SUB2"
case "$CMD" in
"issue view"|"issue list"|"search issues"|"label list")
;;
*)
echo "Error: only 'issue view', 'issue list', 'search issues', 'label list' are allowed (e.g., ./scripts/gh.sh issue view 123)" >&2
exit 1
;;
esac
shift 2
# Separate flags from positional arguments
POSITIONAL=()
FLAGS=()
skip_next=false
for arg in "$@"; do
if [[ "$skip_next" == true ]]; then
FLAGS+=("$arg")
skip_next=false
elif [[ "$arg" == -* ]]; then
flag="${arg%%=*}"
matched=false
for allowed in "${ALLOWED_FLAGS[@]}"; do
if [[ "$flag" == "$allowed" ]]; then
matched=true
break
fi
done
if [[ "$matched" == false ]]; then
echo "Error: only --comments, --state, --limit, --label flags are allowed (e.g., ./scripts/gh.sh issue list --state open --limit 20)" >&2
exit 1
fi
FLAGS+=("$arg")
# If flag expects a value and isn't using = syntax, skip next arg
if [[ "$arg" != *=* ]]; then
for vflag in "${FLAGS_WITH_VALUES[@]}"; do
if [[ "$flag" == "$vflag" ]]; then
skip_next=true
break
fi
done
fi
else
POSITIONAL+=("$arg")
fi
done
if [[ "$CMD" == "search issues" ]]; then
QUERY="${POSITIONAL[0]:-}"
QUERY_LOWER=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]')
if [[ "$QUERY_LOWER" == *"repo:"* || "$QUERY_LOWER" == *"org:"* || "$QUERY_LOWER" == *"user:"* ]]; then
echo "Error: search query must not contain repo:, org:, or user: qualifiers (e.g., ./scripts/gh.sh search issues \"bug report\" --limit 10)" >&2
exit 1
fi
gh "$SUB1" "$SUB2" "$QUERY" --repo "$REPO" "${FLAGS[@]}"
elif [[ "$CMD" == "issue view" ]]; then
if [[ ${#POSITIONAL[@]} -ne 1 ]] || ! [[ "${POSITIONAL[0]}" =~ ^[0-9]+$ ]]; then
echo "Error: issue view requires exactly one numeric issue number (e.g., ./scripts/gh.sh issue view 123)" >&2
exit 1
fi
gh "$SUB1" "$SUB2" "${POSITIONAL[0]}" "${FLAGS[@]}"
else
if [[ ${#POSITIONAL[@]} -ne 0 ]]; then
echo "Error: issue list and label list do not accept positional arguments (e.g., ./scripts/gh.sh issue list --state open, ./scripts/gh.sh label list --limit 100)" >&2
exit 1
fi
gh "$SUB1" "$SUB2" "${FLAGS[@]}"
fi

View File

@@ -1,38 +0,0 @@
// Single source of truth for issue lifecycle labels, timeouts, and messages.
export const lifecycle = [
{
label: "invalid",
days: 3,
reason: "this doesn't appear to be about Claude Code",
nudge: "This doesn't appear to be about [Claude Code](https://github.com/anthropics/claude-code). For general Anthropic support, visit [support.anthropic.com](https://support.anthropic.com).",
},
{
label: "needs-repro",
days: 7,
reason: "we still need reproduction steps to investigate",
nudge: "We weren't able to reproduce this. Could you provide steps to trigger the issue — what you ran, what happened, and what you expected?",
},
{
label: "needs-info",
days: 7,
reason: "we still need a bit more information to move forward",
nudge: "We need more information to continue investigating. Can you make sure to include your Claude Code version (`claude --version`), OS, and any error messages or logs?",
},
{
label: "stale",
days: 14,
reason: "inactive for too long",
nudge: "This issue has been automatically marked as stale due to inactivity.",
},
{
label: "autoclose",
days: 14,
reason: "inactive for too long",
nudge: "This issue has been marked for automatic closure.",
},
] as const;
export type LifecycleLabel = (typeof lifecycle)[number]["label"];
export const STALE_UPVOTE_THRESHOLD = 10;

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env bun
// Posts a comment when a lifecycle label is applied to an issue,
// giving the author a heads-up and a chance to respond before auto-close.
import { lifecycle } from "./issue-lifecycle.ts";
const DRY_RUN = process.argv.includes("--dry-run");
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY; // owner/repo
const label = process.env.LABEL;
const issueNumber = process.env.ISSUE_NUMBER;
if (!DRY_RUN && !token) throw new Error("GITHUB_TOKEN required");
if (!repo) throw new Error("GITHUB_REPOSITORY required");
if (!label) throw new Error("LABEL required");
if (!issueNumber) throw new Error("ISSUE_NUMBER required");
const entry = lifecycle.find((l) => l.label === label);
if (!entry) {
console.log(`No lifecycle entry for label "${label}", skipping`);
process.exit(0);
}
const body = `${entry.nudge} This issue will be closed automatically if there's no activity within ${entry.days} days.`;
// --
if (DRY_RUN) {
console.log(`Would comment on #${issueNumber} for label "${label}":\n\n${body}`);
process.exit(0);
}
const response = await fetch(
`https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github.v3+json",
"Content-Type": "application/json",
"User-Agent": "lifecycle-comment",
},
body: JSON.stringify({ body }),
}
);
if (!response.ok) {
const text = await response.text();
throw new Error(`GitHub API ${response.status}: ${text}`);
}
console.log(`Commented on #${issueNumber} for label "${label}"`);

View File

@@ -1,168 +0,0 @@
#!/usr/bin/env bun
import { lifecycle, STALE_UPVOTE_THRESHOLD } from "./issue-lifecycle.ts";
// --
const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
const DRY_RUN = process.argv.includes("--dry-run");
const CLOSE_MESSAGE = (reason: string) =>
`Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`;
// --
async function githubRequest<T>(
endpoint: string,
method = "GET",
body?: unknown
): Promise<T> {
const token = process.env.GITHUB_TOKEN;
if (!token) throw new Error("GITHUB_TOKEN required");
const response = await fetch(`https://api.github.com${endpoint}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "sweep",
...(body && { "Content-Type": "application/json" }),
},
...(body && { body: JSON.stringify(body) }),
});
if (!response.ok) {
if (response.status === 404) return {} as T;
const text = await response.text();
throw new Error(`GitHub API ${response.status}: ${text}`);
}
return response.json();
}
// --
async function markStale(owner: string, repo: string) {
const staleDays = lifecycle.find((l) => l.label === "stale")!.days;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - staleDays);
let labeled = 0;
console.log(`\n=== marking stale (${staleDays}d inactive) ===`);
for (let page = 1; page <= 10; page++) {
const issues = await githubRequest<any[]>(
`/repos/${owner}/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}`
);
if (issues.length === 0) break;
for (const issue of issues) {
if (issue.pull_request) continue;
if (issue.locked) continue;
if (issue.assignees?.length > 0) continue;
const updatedAt = new Date(issue.updated_at);
if (updatedAt > cutoff) return labeled;
const alreadyStale = issue.labels?.some(
(l: any) => l.name === "stale" || l.name === "autoclose"
);
if (alreadyStale) continue;
const thumbsUp = issue.reactions?.["+1"] ?? 0;
if (thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
if (DRY_RUN) {
const age = Math.floor((Date.now() - updatedAt.getTime()) / 86400000);
console.log(`#${issue.number}: would label stale (${age}d inactive) — ${issue.title}`);
} else {
await githubRequest(`${base}/labels`, "POST", { labels: ["stale"] });
console.log(`#${issue.number}: labeled stale — ${issue.title}`);
}
labeled++;
}
}
return labeled;
}
async function closeExpired(owner: string, repo: string) {
let closed = 0;
for (const { label, days, reason } of lifecycle) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
console.log(`\n=== ${label} (${days}d timeout) ===`);
for (let page = 1; page <= 10; page++) {
const issues = await githubRequest<any[]>(
`/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}`
);
if (issues.length === 0) break;
for (const issue of issues) {
if (issue.pull_request) continue;
if (issue.locked) continue;
const thumbsUp = issue.reactions?.["+1"] ?? 0;
if (thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
const labeledAt = events
.filter((e) => e.event === "labeled" && e.label?.name === label)
.map((e) => new Date(e.created_at))
.pop();
if (!labeledAt || labeledAt > cutoff) continue;
// Skip if a non-bot user commented after the label was applied.
// The triage workflow should remove lifecycle labels on human
// activity, but check here too as a safety net.
const comments = await githubRequest<any[]>(
`${base}/comments?since=${labeledAt.toISOString()}&per_page=100`
);
const hasHumanComment = comments.some(
(c) => c.user && c.user.type !== "Bot"
);
if (hasHumanComment) {
console.log(
`#${issue.number}: skipping (human activity after ${label} label)`
);
continue;
}
if (DRY_RUN) {
const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
} else {
await githubRequest(`${base}/comments`, "POST", { body: CLOSE_MESSAGE(reason) });
await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" });
console.log(`#${issue.number}: closed (${label})`);
}
closed++;
}
}
}
return closed;
}
// --
const owner = process.env.GITHUB_REPOSITORY_OWNER;
const repo = process.env.GITHUB_REPOSITORY_NAME;
if (!owner || !repo)
throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
if (DRY_RUN) console.log("DRY RUN — no changes will be made\n");
const labeled = await markStale(owner, repo);
const closed = await closeExpired(owner, repo);
console.log(`\nDone: ${labeled} ${DRY_RUN ? "would be labeled" : "labeled"} stale, ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);