Compare commits

..

5 Commits

Author SHA1 Message Date
Chris Lloyd
33c8ce5f92 Fix sweep script crashing on locked issues
The sweep job (https://github.com/anthropics/claude-code/actions/runs/21983111029/job/63510453226)
was silently failing when closeExpired tried to comment on a locked issue,
causing a 403 from the GitHub API.

Two issues:

1. closeExpired didn't skip locked issues like markStale already does.
   Adding the same `if (issue.locked) continue` guard fixes this.

2. The error was swallowed by `main().catch(console.error)` which logs
   to stderr but exits 0, so CI reported success despite the crash.
   Replaced the main() wrapper with top-level await so unhandled errors
   properly crash the process with a non-zero exit code.

## Test plan

YOLO
2026-02-13 15:29:45 -08:00
GitHub Actions
b374a30699 chore: Update CHANGELOG.md 2026-02-13 20:01:23 +00:00
GitHub Actions
a01a88d5ee chore: Update CHANGELOG.md 2026-02-13 19:55:44 +00:00
GitHub Actions
42c62d73ce chore: Update CHANGELOG.md 2026-02-13 17:43:37 +00:00
GitHub Actions
1b50583382 chore: Update CHANGELOG.md 2026-02-13 06:07:54 +00:00
4 changed files with 40 additions and 247 deletions

View File

@@ -1,6 +1,14 @@
# Changelog
## 2.1.39
## 2.1.42
- Improved startup performance by deferring Zod schema construction
- Improved prompt cache hit rates by moving date out of system prompt
- Added one-time Opus 4.6 effort callout for eligible users
- Fixed /resume showing interrupt messages as session titles
- Fixed image dimension limit errors to suggest /compact
## 2.1.41
- Added guard against launching Claude Code inside another Claude Code session
- Fixed Agent Teams using wrong model identifier for Bedrock, Vertex, and Foundry customers
@@ -10,11 +18,23 @@
- Fixed plugin browse showing misleading "Space to Toggle" hint for already-installed plugins
- Fixed hook blocking errors (exit code 2) not showing stderr to the user
- Added `speed` attribute to OTel events and trace spans for fast mode visibility
- 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
- Added `claude auth login`, `claude auth status`, and `claude auth logout` CLI subcommands
- Added Windows ARM64 (win32-arm64) native binary support
- Improved `/rename` to auto-generate session name from conversation context when called without arguments
- Improved narrow terminal layout for prompt footer
- Fixed file resolution failing for @-mentions with anchor fragments (e.g., `@README.md#installation`)
- Fixed FileReadTool blocking the process on FIFOs, `/dev/stdin`, and large files
- Fixed background task notifications not being delivered in streaming Agent SDK mode
- Fixed cursor jumping to end on each keystroke in classifier rule input
- Fixed markdown link display text being dropped for raw URL
- Fixed auto-compact failure error notifications being shown to users
- Fixed permission wait time being included in subagent elapsed time display
- Fixed proactive ticks firing while in plan mode
- Fixed clear stale permission rules when settings change on disk
- Fixed hook blocking errors showing stderr content in UI
## 2.1.39
- Improved terminal rendering performance
- Fixed fatal errors being swallowed instead of displayed
- Fixed process hanging after session close

View File

@@ -1,173 +0,0 @@
#!/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}"

View File

@@ -10,40 +10,18 @@ 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:
# 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
# Silently ignore logging errors to avoid disrupting the hook
pass
@@ -180,44 +158,26 @@ 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:
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}")
except (json.JSONDecodeError, IOError):
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 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
except IOError as e:
debug_log(f"Failed to save state file: {e}")
pass # Fail silently if we can't save state
def check_patterns(file_path, content):
@@ -256,8 +216,6 @@ 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")
@@ -265,13 +223,6 @@ 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()

View File

@@ -115,6 +115,7 @@ async function closeExpired(owner: string, repo: string) {
for (const issue of issues) {
if (issue.pull_request) continue;
if (issue.locked) continue;
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
@@ -144,20 +145,14 @@ async function closeExpired(owner: string, repo: string) {
// --
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");
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");
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);
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);
export {};
console.log(`\nDone: ${labeled} ${DRY_RUN ? "would be labeled" : "labeled"} stale, ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);