mirror of
https://github.com/anthropics/claude-code.git
synced 2026-02-19 04:27:33 -08:00
Compare commits
4 Commits
bac22cb316
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
965dbf27eb | ||
|
|
232213304d | ||
|
|
a93966285e | ||
|
|
0931fb76da |
125
.github/workflows/claude-issue-triage.yml
vendored
125
.github/workflows/claude-issue-triage.yml
vendored
@@ -1,13 +1,20 @@
|
||||
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
|
||||
@@ -17,30 +24,6 @@ jobs:
|
||||
- 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
|
||||
@@ -50,56 +33,72 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
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.
|
||||
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 action should be to apply labels.
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only actions are adding or removing labels.
|
||||
|
||||
Issue Information:
|
||||
Context:
|
||||
- REPO: ${{ github.repository }}
|
||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
- EVENT: ${{ github.event_name }}
|
||||
|
||||
TASK OVERVIEW:
|
||||
ALLOWED LABELS — you may ONLY use labels from this list. Never invent new labels.
|
||||
|
||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
TASK:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
**If EVENT is "issues" (new issue):**
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
- Be thorough in your analysis
|
||||
- Only select labels from the provided list above
|
||||
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
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_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 }}
|
||||
claude_args: |
|
||||
--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"
|
||||
--model claude-opus-4-6
|
||||
--allowedTools "Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search issues:*)"
|
||||
|
||||
157
.github/workflows/stale-issue-manager.yml
vendored
157
.github/workflows/stale-issue-manager.yml
vendored
@@ -1,157 +0,0 @@
|
||||
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}`);
|
||||
4
.github/workflows/sweep.yml
vendored
4
.github/workflows/sweep.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: "Daily Issue Sweep"
|
||||
name: "Issue Sweep"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 10 * * *" # 2am Pacific
|
||||
- cron: "0 10,22 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
- Fixed /resume showing interrupt messages as session titles
|
||||
- Fixed Opus 4.6 launch announcement showing for Bedrock/Vertex/Foundry users
|
||||
- Improved error message for many-image dimension limit errors with /compact suggestion
|
||||
- Fixed structured-outputs beta header being sent unconditionally on Vertex/Bedrock
|
||||
- Fixed spurious warnings for non-agent markdown files in `.claude/agents/` directory
|
||||
- Improved terminal rendering performance
|
||||
- Fixed fatal errors being swallowed instead of displayed
|
||||
- Fixed process hanging after session close
|
||||
|
||||
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
|
||||
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 = "/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):
|
||||
"""Append debug message to log file with timestamp."""
|
||||
global _disk_space_warned
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
with open(DEBUG_LOG_FILE, "a") as f:
|
||||
f.write(f"[{timestamp}] {message}\n")
|
||||
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
|
||||
|
||||
|
||||
@@ -158,26 +180,44 @@ def cleanup_old_state_files():
|
||||
|
||||
def load_state(session_id):
|
||||
"""Load the state of shown warnings from file."""
|
||||
global _disk_space_warned
|
||||
state_file = get_state_file(session_id)
|
||||
if os.path.exists(state_file):
|
||||
try:
|
||||
with open(state_file, "r") as 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()
|
||||
|
||||
|
||||
def save_state(session_id, shown_warnings):
|
||||
"""Save the state of shown warnings to file."""
|
||||
global _disk_space_warned
|
||||
state_file = get_state_file(session_id)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
||||
with open(state_file, "w") as f:
|
||||
json.dump(list(shown_warnings), f)
|
||||
except IOError as e:
|
||||
debug_log(f"Failed to save state file: {e}")
|
||||
pass # Fail silently if we can't save state
|
||||
except Exception as e:
|
||||
# Check for disk space errors and provide user-friendly warning
|
||||
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):
|
||||
@@ -216,6 +256,8 @@ def extract_content_from_input(tool_name, tool_input):
|
||||
|
||||
def main():
|
||||
"""Main hook function."""
|
||||
global _disk_space_warned
|
||||
|
||||
# Check if security reminders are enabled
|
||||
security_reminder_enabled = os.environ.get("ENABLE_SECURITY_REMINDER", "1")
|
||||
|
||||
@@ -223,6 +265,13 @@ def main():
|
||||
if security_reminder_enabled == "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)
|
||||
if random.random() < 0.1:
|
||||
cleanup_old_state_files()
|
||||
|
||||
@@ -4,21 +4,20 @@
|
||||
|
||||
const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
|
||||
const DRY_RUN = process.argv.includes("--dry-run");
|
||||
const STALE_DAYS = 14;
|
||||
const STALE_UPVOTE_THRESHOLD = 10;
|
||||
|
||||
const CLOSE_MESSAGE = (reason: string) =>
|
||||
`Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`;
|
||||
|
||||
const lifecycle = [
|
||||
{ label: "needs-repro", days: 7 },
|
||||
{ label: "needs-info", days: 7 },
|
||||
{ label: "needs-votes", days: 30 },
|
||||
{ label: "stale", days: 30 },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
const closeMessages: Record<string, string> = {
|
||||
"needs-repro": `Closing — we weren't able to get the reproduction steps needed to investigate.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with steps to reproduce.`,
|
||||
"needs-info": `Closing — we didn't receive the information needed to move forward.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with the requested details.`,
|
||||
"needs-votes": `Closing this feature request — it didn't get enough community support to prioritize.\n\nIf you'd still like to see this, please [open a new feature request](${NEW_ISSUE}) with more context about the use case.`,
|
||||
stale: `Closing due to inactivity.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with up-to-date information.`,
|
||||
};
|
||||
|
||||
// --
|
||||
|
||||
async function githubRequest<T>(
|
||||
@@ -51,17 +50,59 @@ async function githubRequest<T>(
|
||||
|
||||
// --
|
||||
|
||||
async function main() {
|
||||
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");
|
||||
async function markStale(owner: string, repo: string) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - STALE_DAYS);
|
||||
|
||||
if (DRY_RUN) console.log("DRY RUN — no issues will be closed\n");
|
||||
let labeled = 0;
|
||||
|
||||
console.log(`\n=== marking stale (${STALE_DAYS}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 isEnhancement = issue.labels?.some(
|
||||
(l: any) => l.name === "enhancement"
|
||||
);
|
||||
const thumbsUp = issue.reactions?.["+1"] ?? 0;
|
||||
if (isEnhancement && 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 } of lifecycle) {
|
||||
for (const { label, days, reason } of lifecycle) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
console.log(`\n=== ${label} (${days}d timeout) ===`);
|
||||
@@ -89,7 +130,7 @@ async function main() {
|
||||
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: closeMessages[label] });
|
||||
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})`);
|
||||
}
|
||||
@@ -98,7 +139,23 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nDone: ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
|
||||
return closed;
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
async function main() {
|
||||
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"}`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user