surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
When disk space runs out, Claude Code can become unresponsive or crash without clear feedback. This adds: - New disk_space_utils.py module with: - ENOSPC error detection (errno 28) - User-friendly warning messages with remediation steps - Disk space availability checking - Safe file write/append helpers - Updated security_reminder_hook.py to: - Check disk space at startup and warn users proactively - Detect disk space errors during state file operations - Provide actionable guidance when disk issues are detected The warnings include specific remediation steps (df -h, cleaning /tmp, emptying trash, docker prune) to help users resolve the issue. Slack context: https://anthropic.slack.com/archives/C07VBSHV7EV/p1770941952212839 https://claude.ai/code/session_017ywHZBHvZasAWS6qcKXCb3
| Aplugins/security-guidance/hooks/disk_space_utils.py |
| Mplugins/security-guidance/hooks/security_reminder_hook.py |
| @@ -1,0 +1,173 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +""" | |
| 3 | +Disk space utilities for Claude Code hooks. | |
| 4 | + | |
| 5 | +Provides helper functions to detect and handle disk space issues (ENOSPC errors) | |
| 6 | +in a user-friendly manner. | |
| 7 | +""" | |
| 8 | + | |
| 9 | +import errno | |
| 10 | +import os | |
| 11 | +import sys | |
| 12 | +from typing import Optional, Tuple | |
| 13 | + | |
| 14 | + | |
| 15 | +# ENOSPC errno value (28 on Linux/Mac) | |
| 16 | +ENOSPC_ERRNO = errno.ENOSPC | |
| 17 | + | |
| 18 | + | |
| 19 | +def is_disk_space_error(exception: Exception) -> bool: | |
| 20 | + """Check if an exception is related to disk space issues. | |
| 21 | + | |
| 22 | + Args: | |
| 23 | + exception: The exception to check | |
| 24 | + | |
| 25 | + Returns: | |
| 26 | + True if the exception indicates a disk space issue | |
| 27 | + """ | |
| 28 | + # Check for OSError with ENOSPC errno | |
| 29 | + if isinstance(exception, OSError): | |
| 30 | + if hasattr(exception, 'errno') and exception.errno == ENOSPC_ERRNO: | |
| 31 | + return True | |
| 32 | + # Also check strerror for various disk space error messages | |
| 33 | + if hasattr(exception, 'strerror') and exception.strerror: | |
| 34 | + strerror_lower = exception.strerror.lower() | |
| 35 | + disk_space_indicators = [ | |
| 36 | + 'no space left on device', | |
| 37 | + 'disk quota exceeded', | |
| 38 | + 'not enough space', | |
| 39 | + 'insufficient disk space', | |
| 40 | + ] | |
| 41 | + if any(indicator in strerror_lower for indicator in disk_space_indicators): | |
| 42 | + return True | |
| 43 | + | |
| 44 | + # Check error message string as fallback | |
| 45 | + error_str = str(exception).lower() | |
| 46 | + if 'enospc' in error_str or 'no space left' in error_str: | |
| 47 | + return True | |
| 48 | + | |
| 49 | + return False | |
| 50 | + | |
| 51 | + | |
| 52 | +def get_disk_space_warning() -> str: | |
| 53 | + """Get a user-friendly warning message for disk space issues. | |
| 54 | + | |
| 55 | + Returns: | |
| 56 | + Warning message string | |
| 57 | + """ | |
| 58 | + return ( | |
| 59 | + "WARNING: Disk space issue detected. Your disk may be full or nearly full.\n" | |
| 60 | + "This can cause Claude Code to become unresponsive or crash.\n" | |
| 61 | + "\n" | |
| 62 | + "Recommended actions:\n" | |
| 63 | + " 1. Free up disk space by deleting unnecessary files\n" | |
| 64 | + " 2. Check available space with: df -h\n" | |
| 65 | + " 3. Clean up temporary files: sudo rm -rf /tmp/* (use with caution)\n" | |
| 66 | + " 4. Empty trash/recycle bin\n" | |
| 67 | + " 5. Consider removing old Docker images: docker system prune" | |
| 68 | + ) | |
| 69 | + | |
| 70 | + | |
| 71 | +def check_available_disk_space(path: str = None, min_bytes: int = 10 * 1024 * 1024) -> Tuple[bool, Optional[str]]: | |
| 72 | + """Check if there's sufficient disk space available. | |
| 73 | + | |
| 74 | + Args: | |
| 75 | + path: Path to check (defaults to home directory) | |
| 76 | + min_bytes: Minimum required bytes (default: 10MB) | |
| 77 | + | |
| 78 | + Returns: | |
| 79 | + Tuple of (has_space, warning_message) | |
| 80 | + - has_space: True if sufficient space available | |
| 81 | + - warning_message: Warning string if low on space, None otherwise | |
| 82 | + """ | |
| 83 | + if path is None: | |
| 84 | + path = os.path.expanduser("~") | |
| 85 | + | |
| 86 | + try: | |
| 87 | + # Get disk usage statistics | |
| 88 | + stat = os.statvfs(path) | |
| 89 | + available_bytes = stat.f_frsize * stat.f_bavail | |
| 90 | + | |
| 91 | + if available_bytes < min_bytes: | |
| 92 | + available_mb = available_bytes / (1024 * 1024) | |
| 93 | + required_mb = min_bytes / (1024 * 1024) | |
| 94 | + return False, ( | |
| 95 | + f"Low disk space warning: Only {available_mb:.1f}MB available " | |
| 96 | + f"(recommended minimum: {required_mb:.1f}MB)\n" | |
| 97 | + f"{get_disk_space_warning()}" | |
| 98 | + ) | |
| 99 | + | |
| 100 | + return True, None | |
| 101 | + | |
| 102 | + except (OSError, AttributeError): | |
| 103 | + # os.statvfs not available on all platforms (e.g., Windows) | |
| 104 | + # Return True and let actual write operations fail if there's no space | |
| 105 | + return True, None | |
| 106 | + | |
| 107 | + | |
| 108 | +def safe_write_file(path: str, content: str, warn_on_disk_error: bool = True) -> Tuple[bool, Optional[str]]: | |
| 109 | + """Safely write content to a file with disk space error handling. | |
| 110 | + | |
| 111 | + Args: | |
| 112 | + path: Path to write to | |
| 113 | + content: Content to write | |
| 114 | + warn_on_disk_error: If True, print warning to stderr on disk space errors | |
| 115 | + | |
| 116 | + Returns: | |
| 117 | + Tuple of (success, error_message) | |
| 118 | + - success: True if write succeeded | |
| 119 | + - error_message: Error description if failed, None otherwise | |
| 120 | + """ | |
| 121 | + try: | |
| 122 | + # Ensure directory exists | |
| 123 | + dir_path = os.path.dirname(path) | |
| 124 | + if dir_path: | |
| 125 | + os.makedirs(dir_path, exist_ok=True) | |
| 126 | + | |
| 127 | + with open(path, 'w') as f: | |
| 128 | + f.write(content) | |
| 129 | + | |
| 130 | + return True, None | |
| 131 | + | |
| 132 | + except Exception as e: | |
| 133 | + if is_disk_space_error(e): | |
| 134 | + error_msg = f"Disk space error writing to {path}: {e}\n{get_disk_space_warning()}" | |
| 135 | + if warn_on_disk_error: | |
| 136 | + print(error_msg, file=sys.stderr) | |
| 137 | + return False, error_msg | |
| 138 | + else: | |
| 139 | + return False, f"Error writing to {path}: {e}" | |
| 140 | + | |
| 141 | + | |
| 142 | +def safe_append_file(path: str, content: str, warn_on_disk_error: bool = True) -> Tuple[bool, Optional[str]]: | |
| 143 | + """Safely append content to a file with disk space error handling. | |
| 144 | + | |
| 145 | + Args: | |
| 146 | + path: Path to append to | |
| 147 | + content: Content to append | |
| 148 | + warn_on_disk_error: If True, print warning to stderr on disk space errors | |
| 149 | + | |
| 150 | + Returns: | |
| 151 | + Tuple of (success, error_message) | |
| 152 | + - success: True if append succeeded | |
| 153 | + - error_message: Error description if failed, None otherwise | |
| 154 | + """ | |
| 155 | + try: | |
| 156 | + # Ensure directory exists | |
| 157 | + dir_path = os.path.dirname(path) | |
| 158 | + if dir_path: | |
| 159 | + os.makedirs(dir_path, exist_ok=True) | |
| 160 | + | |
| 161 | + with open(path, 'a') as f: | |
| 162 | + f.write(content) | |
| 163 | + | |
| 164 | + return True, None | |
| 165 | + | |
| 166 | + except Exception as e: | |
| 167 | + if is_disk_space_error(e): | |
| 168 | + error_msg = f"Disk space error appending to {path}: {e}\n{get_disk_space_warning()}" | |
| 169 | + if warn_on_disk_error: | |
| 170 | + print(error_msg, file=sys.stderr) | |
| 171 | + return False, error_msg | |
| 172 | + else: | |
| 173 | + return False, f"Error appending to {path}: {e}" |
| @@ -10,18 +10,40 @@ | ||
| 10 | 10 | import sys |
| 11 | 11 | from datetime import datetime |
| 12 | 12 | |
| 13 | +# Import disk space utilities | |
| 14 | +try: | |
| 15 | + from disk_space_utils import ( | |
| 16 | + is_disk_space_error, | |
| 17 | + get_disk_space_warning, | |
| 18 | + check_available_disk_space, | |
| 19 | + safe_write_file, | |
| 20 | + safe_append_file, | |
| 21 | + ) | |
| 22 | + DISK_UTILS_AVAILABLE = True | |
| 23 | +except ImportError: | |
| 24 | + # Fallback if disk_space_utils not available | |
| 25 | + DISK_UTILS_AVAILABLE = False | |
| 26 | + | |
| 13 | 27 | # Debug log file |
| 14 | 28 | DEBUG_LOG_FILE = "/tmp/security-warnings-log.txt" |
| 15 | 29 | |
| 30 | +# Track if we've already warned about disk space in this session | |
| 31 | +_disk_space_warned = False | |
| 16 | 32 | |
| 33 | + | |
| 17 | 34 | def debug_log(message): |
| 18 | 35 | """Append debug message to log file with timestamp.""" |
| 36 | + global _disk_space_warned | |
| 19 | 37 | try: |
| 20 | 38 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] |
| 21 | 39 | with open(DEBUG_LOG_FILE, "a") as f: |
| 22 | 40 | f.write(f"[{timestamp}] {message}\n") |
| 23 | 41 | except Exception as e: |
| 24 | - # Silently ignore logging errors to avoid disrupting the hook | |
| 42 | + # Check if this is a disk space error and warn the user | |
| 43 | + if DISK_UTILS_AVAILABLE and is_disk_space_error(e) and not _disk_space_warned: | |
| 44 | + _disk_space_warned = True | |
| 45 | + print(f"[Security Hook] {get_disk_space_warning()}", file=sys.stderr) | |
| 46 | + # Continue silently to avoid disrupting the hook | |
| 25 | 47 | pass |
| 26 | 48 | |
| 27 | 49 | |
| @@ -158,26 +180,44 @@ | ||
| 158 | 180 | |
| 159 | 181 | def load_state(session_id): |
| 160 | 182 | """Load the state of shown warnings from file.""" |
| 183 | + global _disk_space_warned | |
| 161 | 184 | state_file = get_state_file(session_id) |
| 162 | 185 | if os.path.exists(state_file): |
| 163 | 186 | try: |
| 164 | 187 | with open(state_file, "r") as f: |
| 165 | 188 | return set(json.load(f)) |
| 166 | - except (json.JSONDecodeError, IOError): | |
| 189 | + except json.JSONDecodeError: | |
| 190 | + debug_log(f"JSON decode error reading state file: {state_file}") | |
| 167 | 191 | return set() |
| 192 | + except Exception as e: | |
| 193 | + # Check for disk-related errors (corrupted filesystem, etc.) | |
| 194 | + if DISK_UTILS_AVAILABLE and is_disk_space_error(e): | |
| 195 | + if not _disk_space_warned: | |
| 196 | + _disk_space_warned = True | |
| 197 | + print(f"[Security Hook] {get_disk_space_warning()}", file=sys.stderr) | |
| 198 | + debug_log(f"Error loading state file: {e}") | |
| 199 | + return set() | |
| 168 | 200 | return set() |
| 169 | 201 | |
| 170 | 202 | |
| 171 | 203 | def save_state(session_id, shown_warnings): |
| 172 | 204 | """Save the state of shown warnings to file.""" |
| 205 | + global _disk_space_warned | |
| 173 | 206 | state_file = get_state_file(session_id) |
| 174 | 207 | try: |
| 175 | 208 | os.makedirs(os.path.dirname(state_file), exist_ok=True) |
| 176 | 209 | with open(state_file, "w") as f: |
| 177 | 210 | json.dump(list(shown_warnings), f) |
| 178 | - except IOError as e: | |
| 179 | - debug_log(f"Failed to save state file: {e}") | |
| 180 | - pass # Fail silently if we can't save state | |
| 211 | + except Exception as e: | |
| 212 | + # Check for disk space errors and provide user-friendly warning | |
| 213 | + if DISK_UTILS_AVAILABLE and is_disk_space_error(e): | |
| 214 | + if not _disk_space_warned: | |
| 215 | + _disk_space_warned = True | |
| 216 | + print(f"[Security Hook] {get_disk_space_warning()}", file=sys.stderr) | |
| 217 | + debug_log(f"Disk space error saving state file: {e}") | |
| 218 | + else: | |
| 219 | + debug_log(f"Failed to save state file: {e}") | |
| 220 | + # Fail silently to not disrupt operation | |
| 181 | 221 | |
| 182 | 222 | |
| 183 | 223 | def check_patterns(file_path, content): |
| @@ -216,6 +256,8 @@ | ||
| 216 | 256 | |
| 217 | 257 | def main(): |
| 218 | 258 | """Main hook function.""" |
| 259 | + global _disk_space_warned | |
| 260 | + | |
| 219 | 261 | # Check if security reminders are enabled |
| 220 | 262 | security_reminder_enabled = os.environ.get("ENABLE_SECURITY_REMINDER", "1") |
| 221 | 263 | |
| @@ -223,6 +265,13 @@ | ||
| 223 | 265 | if security_reminder_enabled == "0": |
| 224 | 266 | sys.exit(0) |
| 225 | 267 | |
| 268 | + # Check for low disk space and warn user (only once per session) | |
| 269 | + if DISK_UTILS_AVAILABLE and not _disk_space_warned: | |
| 270 | + has_space, warning = check_available_disk_space() | |
| 271 | + if not has_space: | |
| 272 | + _disk_space_warned = True | |
| 273 | + print(f"[Security Hook] {warning}", file=sys.stderr) | |
| 274 | + | |
| 226 | 275 | # Periodically clean up old state files (10% chance per run) |
| 227 | 276 | if random.random() < 0.1: |
| 228 | 277 | cleanup_old_state_files() |