Compare commits

...

15 Commits

Author SHA1 Message Date
Claude
34994ca273 Add Python cache files to .gitignore 2025-12-16 21:00:58 +00:00
Claude
ea2608cf85 Add PostToolUse hook example for fixing file permissions
This adds a workaround for issue #12172 where the Write tool ignores
umask and creates files with hardcoded 0600 permissions.

The hook:
- Runs after Write/Edit tool operations
- Detects files with restrictive 0600 permissions
- Applies umask-respecting permissions (e.g., 0644 with umask 022)
- Includes comprehensive test suite with 15 test cases

Users can configure this hook in their settings to fix file permissions
until the underlying issue in the Write tool is resolved.
2025-12-16 20:58:56 +00:00
kashyap murali
c27c6f4e4a Merge pull request #14071 from anthropics/claude/slack-make-no-comment-default-Qqha8
Make no-comment the default for /code-review
2025-12-16 12:22:18 -08:00
GitHub Actions
0dde1fef97 chore: Update CHANGELOG.md 2025-12-15 23:49:39 +00:00
Claude
e4f682030b Make no-comment the default for /code-review
Change the default behavior of /code-review to output to the terminal
instead of posting a PR comment. Users can use the --comment flag to
explicitly post the review as a PR comment when desired.

This is more suitable for local development workflows where posting
comments to the PR is not always needed.
2025-12-15 17:50:32 +00:00
GitHub Actions
eb87245010 chore: Update CHANGELOG.md 2025-12-13 00:59:55 +00:00
GitHub Actions
3680637065 chore: Update CHANGELOG.md 2025-12-12 23:31:50 +00:00
GitHub Actions
2192c86c20 chore: Update CHANGELOG.md 2025-12-12 01:29:45 +00:00
kashyap murali
dfd3494132 Merge pull request #13739 from anthropics/claude/slack-session-01GzKi42xM3SphuxeQ4De88U
Remove footer from code-review plugin output
2025-12-11 14:42:30 -08:00
Claude
e8cca9a7af Remove footer from code-review plugin output
Remove the "Generated with Claude Code" footer and feedback CTA
from the code review comment template as it adds noise without
providing value after the first viewing.
2025-12-11 22:40:32 +00:00
GitHub Actions
6358669884 chore: Update CHANGELOG.md 2025-12-11 19:07:31 +00:00
GitHub Actions
ace0a82778 chore: Update CHANGELOG.md 2025-12-11 04:39:56 +00:00
GitHub Actions
e095e1270a chore: Update CHANGELOG.md 2025-12-10 08:18:47 +00:00
GitHub Actions
69da5e8269 chore: Update CHANGELOG.md 2025-12-10 02:27:37 +00:00
GitHub Actions
7069a25987 chore: Update CHANGELOG.md 2025-12-09 02:08:28 +00:00
6 changed files with 543 additions and 15 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.DS_Store
__pycache__/
*.pyc

View File

@@ -1,5 +1,84 @@
# Changelog
## 2.0.70
- Added Enter key to accept and submit prompt suggestions immediately (tab still accepts for editing)
- Added wildcard syntax `mcp__server__*` for MCP tool permissions to allow or deny all tools from a server
- Added auto-update toggle for plugin marketplaces, allowing per-marketplace control over automatic updates
- Added `plan_mode_required` spawn parameter for teammates to require plan approval before implementing changes
- Added `current_usage` field to status line input, enabling accurate context window percentage calculations
- Fixed input being cleared when processing queued commands while the user was typing
- Fixed prompt suggestions replacing typed input when pressing Tab
- Fixed diff view not updating when terminal is resized
- Improved memory usage by 3x for large conversations
- Improved resolution of stats screenshots copied to clipboard (Ctrl+S) for crisper images
- Removed # shortcut for quick memory entry (tell Claude to edit your CLAUDE.md instead)
- Fix thinking mode toggle in /config not persisting correctly
- Improve UI for file creation permission dialog
## 2.0.69
- Minor bugfixes
## 2.0.68
- Fixed IME (Input Method Editor) support for languages like Chinese, Japanese, and Korean by correctly positioning the composition window at the cursor
- Fixed a bug where disallowed MCP tools were visible to the model
- Fixed an issue where steering messages could be lost while a subagent is working
- Fixed Option+Arrow word navigation treating entire CJK (Chinese, Japanese, Korean) text sequences as a single word instead of navigating by word boundaries
- Improved plan mode exit UX: show simplified yes/no dialog when exiting with empty or missing plan instead of throwing an error
- Add support for enterprise managed settings. Contact your Anthropic account team to enable this feature.
## 2.0.67
- Thinking mode is now enabled by default for Opus 4.5
- Thinking mode configuration has moved to /config
- Added search functionality to `/permissions` command with `/` keyboard shortcut for filtering rules by tool name
- Show reason why autoupdater is disabled in `/doctor`
- Fixed false "Another process is currently updating Claude" error when running `claude update` while another instance is already on the latest version
- Fixed MCP servers from `.mcp.json` being stuck in pending state when running in non-interactive mode (`-p` flag or piped input)
- Fixed scroll position resetting after deleting a permission rule in `/permissions`
- Fixed word deletion (opt+delete) and word navigation (opt+arrow) not working correctly with non-Latin text such as Cyrillic, Greek, Arabic, Hebrew, Thai, and Chinese
- Fixed `claude install --force` not bypassing stale lock files
- Fixed consecutive @~/ file references in CLAUDE.md being incorrectly parsed due to markdown strikethrough interference
- Windows: Fixed plugin MCP servers failing due to colons in log directory paths
## 2.0.65
- Added ability to switch models while writing a prompt using alt+p (linux, windows), option+p (macos).
- Added context window information to status line input
- Added `fileSuggestion` setting for custom `@` file search commands
- Added `CLAUDE_CODE_SHELL` environment variable to override automatic shell detection (useful when login shell differs from actual working shell)
- Fixed prompt not being saved to history when aborting a query with Escape
- Fixed Read tool image handling to identify format from bytes instead of file extension
## 2.0.64
- Made auto-compacting instant
- Agents and bash commands can run asynchronously and send messages to wake up the main agent
- /stats now provides users with interesting CC stats, such as favorite model, usage graph, usage streak
- Added named session support: use `/rename` to name sessions, `/resume <name>` in REPL or `claude --resume <name>` from the terminal to resume them
- Added support for .claude/rules/`. See https://code.claude.com/docs/en/memory for details.
- Added image dimension metadata when images are resized, enabling accurate coordinate mappings for large images
- Fixed auto-loading .env when using native installer
- Fixed `--system-prompt` being ignored when using `--continue` or `--resume` flags
- Improved `/resume` screen with grouped forked sessions and keyboard shortcuts for preview (P) and rename (R)
- VSCode: Added copy-to-clipboard button on code blocks and bash tool inputs
- VSCode: Fixed extension not working on Windows ARM64 by falling back to x64 binary via emulation
- Bedrock: Improve efficiency of token counting
- Bedrock: Add support for `aws login` AWS Management Console credentials
- Unshipped AgentOutputTool and BashOutputTool, in favor of a new unified TaskOutputTool
## 2.0.62
- Added "(Recommended)" indicator for multiple-choice questions, with the recommended option moved to the top of the list
- Added `attribution` setting to customize commit and PR bylines (deprecates `includeCoAuthoredBy`)
- Fixed duplicate slash commands appearing when ~/.claude is symlinked to a project directory
- Fixed slash command selection not working when multiple commands share the same name
- Fixed an issue where skill files inside symlinked skill directories could become circular symlinks
- Fixed running versions getting removed because lock file incorrectly going stale
- Fixed IDE diff tab not closing when rejecting file changes
## 2.0.61
- Reverted VSCode support for multiple terminal clients due to responsiveness issues.

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
Claude Code Hook: Fix File Permissions After Write
===================================================
This hook runs as a PostToolUse hook for the Write and Edit tools.
It fixes file permissions to respect the system's umask setting.
This addresses the issue where Claude Code's Write tool creates files with
restrictive 0600 permissions, ignoring the user's umask setting.
Read more about hooks here: https://docs.anthropic.com/en/docs/claude-code/hooks
Configuration example for ~/.claude/settings.json or .claude/settings.local.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 /path/to/claude-code/examples/hooks/fix_file_permissions_example.py"
}
]
}
]
}
}
How it works:
- After Write or Edit tool completes, this hook runs
- Gets the file path from the tool input
- Calculates the correct permissions based on the current umask
- Applies the umask-respecting permissions to the file
For example:
- With umask 022: files become 0644 (rw-r--r--)
- With umask 002: files become 0664 (rw-rw-r--)
- With umask 077: files remain 0600 (rw-------)
"""
import json
import os
import stat
import sys
def get_umask() -> int:
"""Get the current umask value.
We temporarily set umask to get the current value, then restore it.
This is the standard way to read umask in Python.
"""
current_umask = os.umask(0)
os.umask(current_umask)
return current_umask
def calculate_file_permissions(umask_value: int) -> int:
"""Calculate file permissions based on umask.
Standard Unix behavior: new files start with 0666 base permissions,
then umask is applied to remove bits.
Args:
umask_value: The current umask value (e.g., 0o022)
Returns:
The file permissions after applying umask (e.g., 0o644)
"""
base_permissions = 0o666 # rw-rw-rw-
return base_permissions & ~umask_value
def fix_file_permissions(file_path: str) -> dict:
"""Fix permissions for a file to respect umask.
Args:
file_path: Path to the file to fix
Returns:
Dict with status information
"""
if not file_path:
return {"status": "skipped", "reason": "no file path provided"}
if not os.path.exists(file_path):
return {"status": "skipped", "reason": "file does not exist"}
if not os.path.isfile(file_path):
return {"status": "skipped", "reason": "path is not a file"}
try:
# Get current permissions
current_mode = stat.S_IMODE(os.stat(file_path).st_mode)
# Calculate expected permissions based on umask
umask_value = get_umask()
expected_mode = calculate_file_permissions(umask_value)
# Only change if current permissions are more restrictive than expected
# This handles the case where Write tool sets 0600 instead of umask-based perms
if current_mode == 0o600 and expected_mode != 0o600:
os.chmod(file_path, expected_mode)
return {
"status": "fixed",
"file": file_path,
"old_mode": oct(current_mode),
"new_mode": oct(expected_mode),
"umask": oct(umask_value),
}
else:
return {
"status": "unchanged",
"file": file_path,
"current_mode": oct(current_mode),
"expected_mode": oct(expected_mode),
}
except PermissionError as e:
return {"status": "error", "reason": f"permission denied: {e}"}
except OSError as e:
return {"status": "error", "reason": f"OS error: {e}"}
def main():
"""Main entry point for the PostToolUse hook."""
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
# Exit 0 - don't block on invalid input, just log to stderr
print(f"Warning: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(0)
tool_name = input_data.get("tool_name", "")
# Only process Write and Edit tools
if tool_name not in ("Write", "Edit"):
sys.exit(0)
tool_input = input_data.get("tool_input", {})
file_path = tool_input.get("file_path", "")
if not file_path:
sys.exit(0)
result = fix_file_permissions(file_path)
# Output result as JSON for logging/debugging
# This will appear in the transcript when running with --debug
if result.get("status") == "fixed":
output = {
"systemMessage": f"Fixed file permissions for {file_path}: {result['old_mode']} -> {result['new_mode']} (umask: {result['umask']})"
}
print(json.dumps(output))
# Always exit 0 - this is a PostToolUse hook, we don't want to block
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""
Tests for the fix_file_permissions_example.py hook.
Run these tests with:
python3 examples/hooks/test_fix_file_permissions.py
Or with pytest:
pytest examples/hooks/test_fix_file_permissions.py -v
"""
import json
import os
import stat
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
# Path to the hook script
HOOK_SCRIPT = Path(__file__).parent / "fix_file_permissions_example.py"
class TestFixFilePermissionsHook(unittest.TestCase):
"""Test cases for the file permissions fix hook."""
def setUp(self):
"""Set up test fixtures."""
self.temp_dir = tempfile.mkdtemp()
self.test_file = os.path.join(self.temp_dir, "test_file.txt")
def tearDown(self):
"""Clean up test files."""
if os.path.exists(self.test_file):
os.remove(self.test_file)
if os.path.exists(self.temp_dir):
os.rmdir(self.temp_dir)
def run_hook(self, tool_name: str, file_path: str) -> subprocess.CompletedProcess:
"""Run the hook script with given input."""
input_data = {
"tool_name": tool_name,
"tool_input": {"file_path": file_path},
"session_id": "test-session",
"cwd": os.getcwd(),
}
result = subprocess.run(
[sys.executable, str(HOOK_SCRIPT)],
input=json.dumps(input_data),
capture_output=True,
text=True,
)
return result
def create_file_with_permissions(self, path: str, mode: int) -> None:
"""Create a test file with specific permissions."""
with open(path, "w") as f:
f.write("test content")
os.chmod(path, mode)
def get_file_permissions(self, path: str) -> int:
"""Get the permission bits of a file."""
return stat.S_IMODE(os.stat(path).st_mode)
def test_fixes_restrictive_permissions_with_umask_022(self):
"""Test that 0600 permissions are fixed to 0644 with umask 022."""
# Save and set umask
old_umask = os.umask(0o022)
try:
# Create file with restrictive permissions (simulating Write tool bug)
self.create_file_with_permissions(self.test_file, 0o600)
self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
# Run the hook
result = self.run_hook("Write", self.test_file)
self.assertEqual(result.returncode, 0)
# Check permissions were fixed
expected_mode = 0o644 # 0666 & ~0022
self.assertEqual(
self.get_file_permissions(self.test_file),
expected_mode,
f"Expected {oct(expected_mode)}, got {oct(self.get_file_permissions(self.test_file))}",
)
finally:
os.umask(old_umask)
def test_fixes_restrictive_permissions_with_umask_002(self):
"""Test that 0600 permissions are fixed to 0664 with umask 002."""
# Save and set umask
old_umask = os.umask(0o002)
try:
# Create file with restrictive permissions
self.create_file_with_permissions(self.test_file, 0o600)
self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
# Run the hook
result = self.run_hook("Write", self.test_file)
self.assertEqual(result.returncode, 0)
# Check permissions were fixed
expected_mode = 0o664 # 0666 & ~0002
self.assertEqual(
self.get_file_permissions(self.test_file),
expected_mode,
f"Expected {oct(expected_mode)}, got {oct(self.get_file_permissions(self.test_file))}",
)
finally:
os.umask(old_umask)
def test_preserves_permissions_matching_umask(self):
"""Test that permissions already matching umask are not changed."""
old_umask = os.umask(0o022)
try:
# Create file with correct permissions already
self.create_file_with_permissions(self.test_file, 0o644)
# Run the hook
result = self.run_hook("Write", self.test_file)
self.assertEqual(result.returncode, 0)
# Permissions should be unchanged
self.assertEqual(self.get_file_permissions(self.test_file), 0o644)
finally:
os.umask(old_umask)
def test_respects_umask_077(self):
"""Test that umask 077 results in 0600 (no change needed)."""
old_umask = os.umask(0o077)
try:
# Create file with 0600 permissions
self.create_file_with_permissions(self.test_file, 0o600)
# Run the hook
result = self.run_hook("Write", self.test_file)
self.assertEqual(result.returncode, 0)
# With umask 077, 0600 is correct - should remain unchanged
self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
finally:
os.umask(old_umask)
def test_handles_edit_tool(self):
"""Test that the hook also works for the Edit tool."""
old_umask = os.umask(0o022)
try:
self.create_file_with_permissions(self.test_file, 0o600)
result = self.run_hook("Edit", self.test_file)
self.assertEqual(result.returncode, 0)
self.assertEqual(self.get_file_permissions(self.test_file), 0o644)
finally:
os.umask(old_umask)
def test_ignores_other_tools(self):
"""Test that the hook ignores non-Write/Edit tools."""
old_umask = os.umask(0o022)
try:
self.create_file_with_permissions(self.test_file, 0o600)
result = self.run_hook("Read", self.test_file)
self.assertEqual(result.returncode, 0)
# Permissions should be unchanged for Read tool
self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
finally:
os.umask(old_umask)
def test_handles_nonexistent_file(self):
"""Test that the hook handles non-existent files gracefully."""
result = self.run_hook("Write", "/nonexistent/path/file.txt")
self.assertEqual(result.returncode, 0)
def test_handles_empty_file_path(self):
"""Test that the hook handles empty file path gracefully."""
input_data = {
"tool_name": "Write",
"tool_input": {},
"session_id": "test-session",
}
result = subprocess.run(
[sys.executable, str(HOOK_SCRIPT)],
input=json.dumps(input_data),
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0)
def test_handles_invalid_json(self):
"""Test that the hook handles invalid JSON input gracefully."""
result = subprocess.run(
[sys.executable, str(HOOK_SCRIPT)],
input="not valid json",
capture_output=True,
text=True,
)
# Should exit 0 even with invalid input (don't block the workflow)
self.assertEqual(result.returncode, 0)
self.assertIn("Invalid JSON", result.stderr)
def test_handles_directory_path(self):
"""Test that the hook ignores directory paths."""
result = self.run_hook("Write", self.temp_dir)
self.assertEqual(result.returncode, 0)
def test_outputs_system_message_on_fix(self):
"""Test that the hook outputs a systemMessage when fixing permissions."""
old_umask = os.umask(0o022)
try:
self.create_file_with_permissions(self.test_file, 0o600)
result = self.run_hook("Write", self.test_file)
self.assertEqual(result.returncode, 0)
# Check that stdout contains the systemMessage JSON
if result.stdout.strip():
output = json.loads(result.stdout)
self.assertIn("systemMessage", output)
self.assertIn("Fixed file permissions", output["systemMessage"])
finally:
os.umask(old_umask)
class TestCalculateFilePermissions(unittest.TestCase):
"""Test the calculate_file_permissions function directly."""
def test_umask_022(self):
"""Test permission calculation with umask 022."""
# Import the function from the hook script
import importlib.util
spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
hook = importlib.util.module_from_spec(spec)
spec.loader.exec_module(hook)
result = hook.calculate_file_permissions(0o022)
self.assertEqual(result, 0o644)
def test_umask_002(self):
"""Test permission calculation with umask 002."""
import importlib.util
spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
hook = importlib.util.module_from_spec(spec)
spec.loader.exec_module(hook)
result = hook.calculate_file_permissions(0o002)
self.assertEqual(result, 0o664)
def test_umask_077(self):
"""Test permission calculation with umask 077."""
import importlib.util
spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
hook = importlib.util.module_from_spec(spec)
spec.loader.exec_module(hook)
result = hook.calculate_file_permissions(0o077)
self.assertEqual(result, 0o600)
def test_umask_000(self):
"""Test permission calculation with umask 000."""
import importlib.util
spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
hook = importlib.util.module_from_spec(spec)
spec.loader.exec_module(hook)
result = hook.calculate_file_permissions(0o000)
self.assertEqual(result, 0o666)
if __name__ == "__main__":
unittest.main()

View File

@@ -22,23 +22,29 @@ Performs automated code review on a pull request using multiple specialized agen
- **Agent #4**: Analyze git blame/history for context-based issues
5. Scores each issue 0-100 for confidence level
6. Filters out issues below 80 confidence threshold
7. Posts review comment with high-confidence issues only
7. Outputs review (to terminal by default, or as PR comment with `--comment` flag)
**Usage:**
```bash
/code-review
/code-review [--comment]
```
**Options:**
- `--comment`: Post the review as a comment on the pull request (default: outputs to terminal only)
**Example workflow:**
```bash
# On a PR branch, run:
# On a PR branch, run locally (outputs to terminal):
/code-review
# Post review as PR comment:
/code-review --comment
# Claude will:
# - Launch 4 review agents in parallel
# - Score each issue for confidence
# - Post comment with issues ≥80 confidence
# - Skip posting if no high-confidence issues found
# - Output issues ≥80 confidence (to terminal or PR depending on flag)
# - Skip if no high-confidence issues found
```
**Features:**
@@ -114,17 +120,23 @@ This plugin is included in the Claude Code repository. The command is automatica
### Standard PR review workflow:
```bash
# Create PR with changes
# Run local review (outputs to terminal)
/code-review
# Review the automated feedback
# Make any necessary fixes
# Optionally post as PR comment
/code-review --comment
# Merge when ready
```
### As part of CI/CD:
```bash
# Trigger on PR creation or update
# Automatically posts review comments
# Use --comment flag to post review comments
/code-review --comment
# Skip if review already exists
```

View File

@@ -52,7 +52,9 @@ Note: Still review Claude generated PR's.
6. Filter out any issues that were not validated in step 5. This step will give us our list of high signal issues for our review.
7. Finally, comment on the pull request.
7. Finally, output the review.
- If the `--comment` argument is provided, post the review as a comment on the pull request using `gh pr comment`
- Otherwise (default), output the review directly to the terminal for local viewing
When writing your comment, follow these guidelines:
a. Keep your output brief
b. Avoid emojis
@@ -93,11 +95,6 @@ Found 3 issues:
<link to file and line with full sha1 + line range for context>
🤖 Generated with [Claude Code](https://claude.ai/code)
<sub>- If this code review was useful, please react with 👍. Otherwise, react with 👎.</sub>
---
- Or, if you found no issues:
@@ -108,8 +105,6 @@ Found 3 issues:
No issues found. Checked for bugs and CLAUDE.md compliance.
🤖 Generated with [Claude Code](https://claude.ai/code)
---
- When linking to code, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-code/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15