mirror of
https://github.com/anthropics/claude-code.git
synced 2026-04-29 16:52:46 +00:00
Compare commits
1 Commits
main
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
965dbf27eb |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/claude-code-marketplace.json",
|
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||||
"name": "claude-code-plugins",
|
"name": "claude-code-plugins",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Bundled plugins for Claude Code including Agent SDK development tools, PR review toolkit, and commit workflows",
|
"description": "Bundled plugins for Claude Code including Agent SDK development tools, PR review toolkit, and commit workflows",
|
||||||
|
|||||||
@@ -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
|
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.
|
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:
|
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):
|
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:
|
- Use `gh` to interact with Github, rather than web fetch
|
||||||
- `./scripts/gh.sh issue view 123` — view an issue
|
- Do not use other tools, beyond `gh` and the comment script (eg. don't use other MCP servers, file edit, etc.)
|
||||||
- `./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.)
|
|
||||||
- Make a todo list first
|
- Make a todo list first
|
||||||
|
|||||||
40
.claude/commands/oncall-triage.md
Normal file
40
.claude/commands/oncall-triage.md
Normal 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
|
||||||
@@ -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.
|
|
||||||
14
.github/workflows/claude-dedupe-issues.yml
vendored
14
.github/workflows/claude-dedupe-issues.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -26,7 +27,6 @@ jobs:
|
|||||||
uses: anthropics/claude-code-action@v1
|
uses: anthropics/claude-code-action@v1
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CLAUDE_CODE_SCRIPT_CAPS: '{"comment-on-duplicates.sh":1}'
|
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
allowed_non_write_users: "*"
|
allowed_non_write_users: "*"
|
||||||
@@ -38,11 +38,10 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
env:
|
env:
|
||||||
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }}
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
TRIGGERED_BY: ${{ github.event_name }}
|
|
||||||
WORKFLOW_RUN_ID: ${{ github.run_id }}
|
|
||||||
run: |
|
run: |
|
||||||
|
ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }}
|
||||||
|
REPO=${{ github.repository }}
|
||||||
|
|
||||||
if [ -z "$STATSIG_API_KEY" ]; then
|
if [ -z "$STATSIG_API_KEY" ]; then
|
||||||
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
echo "STATSIG_API_KEY not found, skipping Statsig logging"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -52,8 +51,7 @@ jobs:
|
|||||||
EVENT_PAYLOAD=$(jq -n \
|
EVENT_PAYLOAD=$(jq -n \
|
||||||
--arg issue_number "$ISSUE_NUMBER" \
|
--arg issue_number "$ISSUE_NUMBER" \
|
||||||
--arg repo "$REPO" \
|
--arg repo "$REPO" \
|
||||||
--arg triggered_by "$TRIGGERED_BY" \
|
--arg triggered_by "${{ github.event_name }}" \
|
||||||
--arg workflow_run_id "$WORKFLOW_RUN_ID" \
|
|
||||||
'{
|
'{
|
||||||
events: [{
|
events: [{
|
||||||
eventName: "github_duplicate_comment_added",
|
eventName: "github_duplicate_comment_added",
|
||||||
@@ -62,7 +60,7 @@ jobs:
|
|||||||
repository: $repo,
|
repository: $repo,
|
||||||
issue_number: ($issue_number | tonumber),
|
issue_number: ($issue_number | tonumber),
|
||||||
triggered_by: $triggered_by,
|
triggered_by: $triggered_by,
|
||||||
workflow_run_id: $workflow_run_id
|
workflow_run_id: "${{ github.run_id }}"
|
||||||
},
|
},
|
||||||
time: (now | floor | tostring)
|
time: (now | floor | tostring)
|
||||||
}]
|
}]
|
||||||
|
|||||||
71
.github/workflows/claude-issue-triage.yml
vendored
71
.github/workflows/claude-issue-triage.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -28,12 +29,76 @@ jobs:
|
|||||||
uses: anthropics/claude-code-action@v1
|
uses: anthropics/claude-code-action@v1
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GH_REPO: ${{ github.repository }}
|
|
||||||
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
|
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
allowed_non_write_users: "*"
|
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. 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:
|
||||||
|
- REPO: ${{ github.repository }}
|
||||||
|
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
- EVENT: ${{ github.event_name }}
|
||||||
|
|
||||||
|
ALLOWED LABELS — you may ONLY use labels from this list. Never invent new labels.
|
||||||
|
|
||||||
|
Type: bug, enhancement, question, documentation, duplicate, invalid
|
||||||
|
Lifecycle: needs-repro, needs-info
|
||||||
|
Platform: platform:linux, platform:macos, platform:windows, platform:wsl, platform:ios, platform:android, platform:vscode, platform:intellij, platform:web, platform:aws-bedrock
|
||||||
|
API: api:bedrock, api:vertex
|
||||||
|
|
||||||
|
TOOLS:
|
||||||
|
- `gh issue view NUMBER`: Read the issue title, body, and labels
|
||||||
|
- `gh issue view NUMBER --comments`: Read the conversation
|
||||||
|
- `gh search issues QUERY`: Find similar or duplicate issues
|
||||||
|
- `gh issue edit NUMBER --add-label` / `--remove-label`: Add or remove labels
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
|
||||||
|
1. Run `gh issue view ${{ github.event.issue.number }}` to read the issue details.
|
||||||
|
2. Run `gh issue view ${{ github.event.issue.number }} --comments` to read the conversation.
|
||||||
|
|
||||||
|
**If EVENT is "issues" (new issue):**
|
||||||
|
|
||||||
|
3. First, check if this issue is actually about Claude Code (the CLI/IDE tool). Issues about the Claude API, claude.ai, the Claude app, Anthropic billing, or other Anthropic products should be labeled `invalid`. If invalid, apply only that label and stop.
|
||||||
|
|
||||||
|
4. Analyze and apply category labels:
|
||||||
|
- Type (bug, enhancement, question, etc.)
|
||||||
|
- Technical areas and platform
|
||||||
|
- Check for duplicates with `gh search issues`. Only mark as duplicate of OPEN issues.
|
||||||
|
|
||||||
|
5. 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.
|
||||||
|
|
||||||
|
6. Apply all selected labels:
|
||||||
|
`gh issue edit ${{ github.event.issue.number }} --add-label "label1" --add-label "label2"`
|
||||||
|
|
||||||
|
**If EVENT is "issue_comment" (comment on existing issue):**
|
||||||
|
|
||||||
|
3. Evaluate lifecycle labels based on the full conversation:
|
||||||
|
- If the issue has `needs-repro` or `needs-info` and the missing information has now been provided, remove the label:
|
||||||
|
`gh issue edit ${{ github.event.issue.number }} --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 labels when substantive details are actually provided.
|
||||||
|
- Do NOT add or remove category labels (bug, enhancement, etc.) on comment events.
|
||||||
|
|
||||||
|
GUIDELINES:
|
||||||
|
- ONLY use labels from the ALLOWED LABELS list above — 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
|
||||||
|
- It's okay to not add any labels if none are clearly applicable
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
claude_args: |
|
claude_args: |
|
||||||
--model claude-opus-4-6
|
--model claude-opus-4-6
|
||||||
|
--allowedTools "Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search issues:*)"
|
||||||
|
|||||||
27
.github/workflows/issue-lifecycle-comment.yml
vendored
27
.github/workflows/issue-lifecycle-comment.yml
vendored
@@ -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 }}
|
|
||||||
47
.github/workflows/non-write-users-check.yml
vendored
47
.github/workflows/non-write-users-check.yml
vendored
@@ -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
118
.github/workflows/oncall-triage.yml
vendored
Normal 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"
|
||||||
1708
CHANGELOG.md
1708
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -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.
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"disableBypassPermissionsMode": "disable"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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"
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Settings Examples
|
# 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`).
|
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`
|
- 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
|
- 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
|
## Full Documentation
|
||||||
|
|
||||||
See https://code.claude.com/docs/en/settings for complete documentation on all available managed settings.
|
See https://code.claude.com/docs/en/settings for complete documentation on all available managed settings.
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Note: Still review Claude generated PR's.
|
|||||||
|
|
||||||
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.
|
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
|
- Provide a brief description of the issue
|
||||||
- For small, self-contained fixes, include a committable suggestion block
|
- 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
|
- For larger fixes (6+ lines, structural changes, or changes spanning multiple locations), describe the issue and suggested fix without a suggestion block
|
||||||
|
|||||||
173
plugins/security-guidance/hooks/disk_space_utils.py
Normal file
173
plugins/security-guidance/hooks/disk_space_utils.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Disk space utilities for Claude Code hooks.
|
||||||
|
|
||||||
|
Provides helper functions to detect and handle disk space issues (ENOSPC errors)
|
||||||
|
in a user-friendly manner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
# ENOSPC errno value (28 on Linux/Mac)
|
||||||
|
ENOSPC_ERRNO = errno.ENOSPC
|
||||||
|
|
||||||
|
|
||||||
|
def is_disk_space_error(exception: Exception) -> bool:
|
||||||
|
"""Check if an exception is related to disk space issues.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exception: The exception to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the exception indicates a disk space issue
|
||||||
|
"""
|
||||||
|
# Check for OSError with ENOSPC errno
|
||||||
|
if isinstance(exception, OSError):
|
||||||
|
if hasattr(exception, 'errno') and exception.errno == ENOSPC_ERRNO:
|
||||||
|
return True
|
||||||
|
# Also check strerror for various disk space error messages
|
||||||
|
if hasattr(exception, 'strerror') and exception.strerror:
|
||||||
|
strerror_lower = exception.strerror.lower()
|
||||||
|
disk_space_indicators = [
|
||||||
|
'no space left on device',
|
||||||
|
'disk quota exceeded',
|
||||||
|
'not enough space',
|
||||||
|
'insufficient disk space',
|
||||||
|
]
|
||||||
|
if any(indicator in strerror_lower for indicator in disk_space_indicators):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check error message string as fallback
|
||||||
|
error_str = str(exception).lower()
|
||||||
|
if 'enospc' in error_str or 'no space left' in error_str:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_disk_space_warning() -> str:
|
||||||
|
"""Get a user-friendly warning message for disk space issues.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Warning message string
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
"WARNING: Disk space issue detected. Your disk may be full or nearly full.\n"
|
||||||
|
"This can cause Claude Code to become unresponsive or crash.\n"
|
||||||
|
"\n"
|
||||||
|
"Recommended actions:\n"
|
||||||
|
" 1. Free up disk space by deleting unnecessary files\n"
|
||||||
|
" 2. Check available space with: df -h\n"
|
||||||
|
" 3. Clean up temporary files: sudo rm -rf /tmp/* (use with caution)\n"
|
||||||
|
" 4. Empty trash/recycle bin\n"
|
||||||
|
" 5. Consider removing old Docker images: docker system prune"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_available_disk_space(path: str = None, min_bytes: int = 10 * 1024 * 1024) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Check if there's sufficient disk space available.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to check (defaults to home directory)
|
||||||
|
min_bytes: Minimum required bytes (default: 10MB)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (has_space, warning_message)
|
||||||
|
- has_space: True if sufficient space available
|
||||||
|
- warning_message: Warning string if low on space, None otherwise
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
path = os.path.expanduser("~")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get disk usage statistics
|
||||||
|
stat = os.statvfs(path)
|
||||||
|
available_bytes = stat.f_frsize * stat.f_bavail
|
||||||
|
|
||||||
|
if available_bytes < min_bytes:
|
||||||
|
available_mb = available_bytes / (1024 * 1024)
|
||||||
|
required_mb = min_bytes / (1024 * 1024)
|
||||||
|
return False, (
|
||||||
|
f"Low disk space warning: Only {available_mb:.1f}MB available "
|
||||||
|
f"(recommended minimum: {required_mb:.1f}MB)\n"
|
||||||
|
f"{get_disk_space_warning()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
except (OSError, AttributeError):
|
||||||
|
# os.statvfs not available on all platforms (e.g., Windows)
|
||||||
|
# Return True and let actual write operations fail if there's no space
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def safe_write_file(path: str, content: str, warn_on_disk_error: bool = True) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Safely write content to a file with disk space error handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to write to
|
||||||
|
content: Content to write
|
||||||
|
warn_on_disk_error: If True, print warning to stderr on disk space errors
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, error_message)
|
||||||
|
- success: True if write succeeded
|
||||||
|
- error_message: Error description if failed, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
dir_path = os.path.dirname(path)
|
||||||
|
if dir_path:
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if is_disk_space_error(e):
|
||||||
|
error_msg = f"Disk space error writing to {path}: {e}\n{get_disk_space_warning()}"
|
||||||
|
if warn_on_disk_error:
|
||||||
|
print(error_msg, file=sys.stderr)
|
||||||
|
return False, error_msg
|
||||||
|
else:
|
||||||
|
return False, f"Error writing to {path}: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def safe_append_file(path: str, content: str, warn_on_disk_error: bool = True) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Safely append content to a file with disk space error handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to append to
|
||||||
|
content: Content to append
|
||||||
|
warn_on_disk_error: If True, print warning to stderr on disk space errors
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, error_message)
|
||||||
|
- success: True if append succeeded
|
||||||
|
- error_message: Error description if failed, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
dir_path = os.path.dirname(path)
|
||||||
|
if dir_path:
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
with open(path, 'a') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if is_disk_space_error(e):
|
||||||
|
error_msg = f"Disk space error appending to {path}: {e}\n{get_disk_space_warning()}"
|
||||||
|
if warn_on_disk_error:
|
||||||
|
print(error_msg, file=sys.stderr)
|
||||||
|
return False, error_msg
|
||||||
|
else:
|
||||||
|
return False, f"Error appending to {path}: {e}"
|
||||||
@@ -10,18 +10,40 @@ import random
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Import disk space utilities
|
||||||
|
try:
|
||||||
|
from disk_space_utils import (
|
||||||
|
is_disk_space_error,
|
||||||
|
get_disk_space_warning,
|
||||||
|
check_available_disk_space,
|
||||||
|
safe_write_file,
|
||||||
|
safe_append_file,
|
||||||
|
)
|
||||||
|
DISK_UTILS_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
# Fallback if disk_space_utils not available
|
||||||
|
DISK_UTILS_AVAILABLE = False
|
||||||
|
|
||||||
# Debug log file
|
# Debug log file
|
||||||
DEBUG_LOG_FILE = "/tmp/security-warnings-log.txt"
|
DEBUG_LOG_FILE = "/tmp/security-warnings-log.txt"
|
||||||
|
|
||||||
|
# Track if we've already warned about disk space in this session
|
||||||
|
_disk_space_warned = False
|
||||||
|
|
||||||
|
|
||||||
def debug_log(message):
|
def debug_log(message):
|
||||||
"""Append debug message to log file with timestamp."""
|
"""Append debug message to log file with timestamp."""
|
||||||
|
global _disk_space_warned
|
||||||
try:
|
try:
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||||
with open(DEBUG_LOG_FILE, "a") as f:
|
with open(DEBUG_LOG_FILE, "a") as f:
|
||||||
f.write(f"[{timestamp}] {message}\n")
|
f.write(f"[{timestamp}] {message}\n")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Silently ignore logging errors to avoid disrupting the hook
|
# Check if this is a disk space error and warn the user
|
||||||
|
if DISK_UTILS_AVAILABLE and is_disk_space_error(e) and not _disk_space_warned:
|
||||||
|
_disk_space_warned = True
|
||||||
|
print(f"[Security Hook] {get_disk_space_warning()}", file=sys.stderr)
|
||||||
|
# Continue silently to avoid disrupting the hook
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -158,26 +180,44 @@ def cleanup_old_state_files():
|
|||||||
|
|
||||||
def load_state(session_id):
|
def load_state(session_id):
|
||||||
"""Load the state of shown warnings from file."""
|
"""Load the state of shown warnings from file."""
|
||||||
|
global _disk_space_warned
|
||||||
state_file = get_state_file(session_id)
|
state_file = get_state_file(session_id)
|
||||||
if os.path.exists(state_file):
|
if os.path.exists(state_file):
|
||||||
try:
|
try:
|
||||||
with open(state_file, "r") as f:
|
with open(state_file, "r") as f:
|
||||||
return set(json.load(f))
|
return set(json.load(f))
|
||||||
except (json.JSONDecodeError, IOError):
|
except json.JSONDecodeError:
|
||||||
|
debug_log(f"JSON decode error reading state file: {state_file}")
|
||||||
|
return set()
|
||||||
|
except Exception as e:
|
||||||
|
# Check for disk-related errors (corrupted filesystem, etc.)
|
||||||
|
if DISK_UTILS_AVAILABLE and is_disk_space_error(e):
|
||||||
|
if not _disk_space_warned:
|
||||||
|
_disk_space_warned = True
|
||||||
|
print(f"[Security Hook] {get_disk_space_warning()}", file=sys.stderr)
|
||||||
|
debug_log(f"Error loading state file: {e}")
|
||||||
return set()
|
return set()
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def save_state(session_id, shown_warnings):
|
def save_state(session_id, shown_warnings):
|
||||||
"""Save the state of shown warnings to file."""
|
"""Save the state of shown warnings to file."""
|
||||||
|
global _disk_space_warned
|
||||||
state_file = get_state_file(session_id)
|
state_file = get_state_file(session_id)
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
||||||
with open(state_file, "w") as f:
|
with open(state_file, "w") as f:
|
||||||
json.dump(list(shown_warnings), f)
|
json.dump(list(shown_warnings), f)
|
||||||
except IOError as e:
|
except Exception as e:
|
||||||
debug_log(f"Failed to save state file: {e}")
|
# Check for disk space errors and provide user-friendly warning
|
||||||
pass # Fail silently if we can't save state
|
if DISK_UTILS_AVAILABLE and is_disk_space_error(e):
|
||||||
|
if not _disk_space_warned:
|
||||||
|
_disk_space_warned = True
|
||||||
|
print(f"[Security Hook] {get_disk_space_warning()}", file=sys.stderr)
|
||||||
|
debug_log(f"Disk space error saving state file: {e}")
|
||||||
|
else:
|
||||||
|
debug_log(f"Failed to save state file: {e}")
|
||||||
|
# Fail silently to not disrupt operation
|
||||||
|
|
||||||
|
|
||||||
def check_patterns(file_path, content):
|
def check_patterns(file_path, content):
|
||||||
@@ -216,6 +256,8 @@ def extract_content_from_input(tool_name, tool_input):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main hook function."""
|
"""Main hook function."""
|
||||||
|
global _disk_space_warned
|
||||||
|
|
||||||
# Check if security reminders are enabled
|
# Check if security reminders are enabled
|
||||||
security_reminder_enabled = os.environ.get("ENABLE_SECURITY_REMINDER", "1")
|
security_reminder_enabled = os.environ.get("ENABLE_SECURITY_REMINDER", "1")
|
||||||
|
|
||||||
@@ -223,6 +265,13 @@ def main():
|
|||||||
if security_reminder_enabled == "0":
|
if security_reminder_enabled == "0":
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Check for low disk space and warn user (only once per session)
|
||||||
|
if DISK_UTILS_AVAILABLE and not _disk_space_warned:
|
||||||
|
has_space, warning = check_available_disk_space()
|
||||||
|
if not has_space:
|
||||||
|
_disk_space_warned = True
|
||||||
|
print(f"[Security Hook] {warning}", file=sys.stderr)
|
||||||
|
|
||||||
# Periodically clean up old state files (10% chance per run)
|
# Periodically clean up old state files (10% chance per run)
|
||||||
if random.random() < 0.1:
|
if random.random() < 0.1:
|
||||||
cleanup_old_state_files()
|
cleanup_old_state_files()
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Comments on a GitHub issue with a list of potential duplicates.
|
# Comments on a GitHub issue with a list of potential duplicates.
|
||||||
# Usage: ./comment-on-duplicates.sh --potential-duplicates 456 789 101
|
# Usage: ./comment-on-duplicates.sh --base-issue 123 --potential-duplicates 456 789 101
|
||||||
#
|
|
||||||
# The base issue number is read from the workflow event payload.
|
|
||||||
#
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="anthropics/claude-code"
|
REPO="anthropics/claude-code"
|
||||||
|
BASE_ISSUE=""
|
||||||
# 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
|
|
||||||
|
|
||||||
DUPLICATES=()
|
DUPLICATES=()
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
--base-issue)
|
||||||
|
BASE_ISSUE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--potential-duplicates)
|
--potential-duplicates)
|
||||||
shift
|
shift
|
||||||
while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
|
while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
|
||||||
@@ -31,12 +25,23 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown argument (only --potential-duplicates is accepted)" >&2
|
echo "Unknown option: $1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
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
|
# Validate duplicates
|
||||||
if [[ ${#DUPLICATES[@]} -eq 0 ]]; then
|
if [[ ${#DUPLICATES[@]} -eq 0 ]]; then
|
||||||
echo "Error: --potential-duplicates requires at least one issue number" >&2
|
echo "Error: --potential-duplicates requires at least one issue number" >&2
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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;
|
|
||||||
@@ -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}"`);
|
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
#!/usr/bin/env bun
|
#!/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 NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
|
||||||
const DRY_RUN = process.argv.includes("--dry-run");
|
const DRY_RUN = process.argv.includes("--dry-run");
|
||||||
|
const STALE_DAYS = 14;
|
||||||
|
const STALE_UPVOTE_THRESHOLD = 10;
|
||||||
|
|
||||||
const CLOSE_MESSAGE = (reason: string) =>
|
const CLOSE_MESSAGE = (reason: string) =>
|
||||||
`Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`;
|
`Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`;
|
||||||
|
|
||||||
|
const lifecycle = [
|
||||||
|
{ label: "invalid", days: 3, reason: "this doesn't appear to be about Claude Code" },
|
||||||
|
{ label: "needs-repro", days: 7, reason: "we still need reproduction steps to investigate" },
|
||||||
|
{ label: "needs-info", days: 7, reason: "we still need a bit more information to move forward" },
|
||||||
|
{ label: "stale", days: 14, reason: "inactive for too long" },
|
||||||
|
{ label: "autoclose", days: 14, reason: "inactive for too long" },
|
||||||
|
];
|
||||||
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
async function githubRequest<T>(
|
async function githubRequest<T>(
|
||||||
@@ -43,13 +51,12 @@ async function githubRequest<T>(
|
|||||||
// --
|
// --
|
||||||
|
|
||||||
async function markStale(owner: string, repo: string) {
|
async function markStale(owner: string, repo: string) {
|
||||||
const staleDays = lifecycle.find((l) => l.label === "stale")!.days;
|
|
||||||
const cutoff = new Date();
|
const cutoff = new Date();
|
||||||
cutoff.setDate(cutoff.getDate() - staleDays);
|
cutoff.setDate(cutoff.getDate() - STALE_DAYS);
|
||||||
|
|
||||||
let labeled = 0;
|
let labeled = 0;
|
||||||
|
|
||||||
console.log(`\n=== marking stale (${staleDays}d inactive) ===`);
|
console.log(`\n=== marking stale (${STALE_DAYS}d inactive) ===`);
|
||||||
|
|
||||||
for (let page = 1; page <= 10; page++) {
|
for (let page = 1; page <= 10; page++) {
|
||||||
const issues = await githubRequest<any[]>(
|
const issues = await githubRequest<any[]>(
|
||||||
@@ -70,8 +77,11 @@ async function markStale(owner: string, repo: string) {
|
|||||||
);
|
);
|
||||||
if (alreadyStale) continue;
|
if (alreadyStale) continue;
|
||||||
|
|
||||||
|
const isEnhancement = issue.labels?.some(
|
||||||
|
(l: any) => l.name === "enhancement"
|
||||||
|
);
|
||||||
const thumbsUp = issue.reactions?.["+1"] ?? 0;
|
const thumbsUp = issue.reactions?.["+1"] ?? 0;
|
||||||
if (thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
|
if (isEnhancement && thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
|
||||||
|
|
||||||
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
||||||
|
|
||||||
@@ -105,11 +115,6 @@ async function closeExpired(owner: string, repo: string) {
|
|||||||
|
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
if (issue.pull_request) continue;
|
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 base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
||||||
|
|
||||||
const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
|
const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
|
||||||
@@ -121,22 +126,6 @@ async function closeExpired(owner: string, repo: string) {
|
|||||||
|
|
||||||
if (!labeledAt || labeledAt > cutoff) continue;
|
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) {
|
if (DRY_RUN) {
|
||||||
const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
|
const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
|
||||||
console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
|
console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
|
||||||
@@ -155,14 +144,20 @@ async function closeExpired(owner: string, repo: string) {
|
|||||||
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
const owner = process.env.GITHUB_REPOSITORY_OWNER;
|
async function main() {
|
||||||
const repo = process.env.GITHUB_REPOSITORY_NAME;
|
const owner = process.env.GITHUB_REPOSITORY_OWNER;
|
||||||
if (!owner || !repo)
|
const repo = process.env.GITHUB_REPOSITORY_NAME;
|
||||||
throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
|
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");
|
if (DRY_RUN) console.log("DRY RUN — no changes will be made\n");
|
||||||
|
|
||||||
const labeled = await markStale(owner, repo);
|
const labeled = await markStale(owner, repo);
|
||||||
const closed = await closeExpired(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"}`);
|
console.log(`\nDone: ${labeled} ${DRY_RUN ? "would be labeled" : "labeled"} stale, ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user