claude-code-tools 0.1.8__tar.gz → 0.1.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-code-tools might be problematic. Click here for more details.
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/PKG-INFO +1 -1
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/claude_code_tools/__init__.py +1 -1
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/claude_code_tools/tmux_cli_controller.py +69 -11
- claude_code_tools-0.1.10/claude_code_tools/tmux_remote_controller.py +69 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/pyproject.toml +6 -2
- claude_code_tools-0.1.8/Makefile +0 -36
- claude_code_tools-0.1.8/hooks/README.md +0 -211
- claude_code_tools-0.1.8/hooks/bash_hook.py +0 -66
- claude_code_tools-0.1.8/hooks/file_size_conditional_hook.py +0 -96
- claude_code_tools-0.1.8/hooks/git_add_block_hook.py +0 -73
- claude_code_tools-0.1.8/hooks/git_checkout_safety_hook.py +0 -126
- claude_code_tools-0.1.8/hooks/grep_block_hook.py +0 -12
- claude_code_tools-0.1.8/hooks/notification_hook.sh +0 -2
- claude_code_tools-0.1.8/hooks/posttask_subtask_flag.py +0 -12
- claude_code_tools-0.1.8/hooks/pretask_subtask_flag.py +0 -12
- claude_code_tools-0.1.8/hooks/rm_block_hook.py +0 -58
- claude_code_tools-0.1.8/hooks/settings.sample.json +0 -64
- claude_code_tools-0.1.8/scripts/fcs-function.sh +0 -13
- claude_code_tools-0.1.8/uv.lock +0 -362
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/.gitignore +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/README.md +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/docs/find-claude-session.md +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/docs/vault-documentation.md +0 -0
{claude_code_tools-0.1.8 → claude_code_tools-0.1.10}/claude_code_tools/tmux_cli_controller.py
RENAMED
|
@@ -11,7 +11,74 @@ from typing import Optional, List, Dict, Tuple, Callable, Union
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import hashlib
|
|
14
|
-
|
|
14
|
+
import importlib.resources
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_help_text():
|
|
18
|
+
"""Load help text from the package's docs directory."""
|
|
19
|
+
try:
|
|
20
|
+
# For development, try to load from the actual file system first
|
|
21
|
+
import pathlib
|
|
22
|
+
module_dir = pathlib.Path(__file__).parent
|
|
23
|
+
# Try looking in the parent directory (repo root) for docs
|
|
24
|
+
docs_file = module_dir.parent / 'docs' / 'tmux-cli-instructions.md'
|
|
25
|
+
if docs_file.exists():
|
|
26
|
+
return docs_file.read_text(encoding='utf-8')
|
|
27
|
+
|
|
28
|
+
# For installed packages, use importlib.resources
|
|
29
|
+
if hasattr(importlib.resources, 'files'):
|
|
30
|
+
# Python 3.9+ style
|
|
31
|
+
import importlib.resources as resources
|
|
32
|
+
|
|
33
|
+
# Try different possible locations for the docs
|
|
34
|
+
# 1. Try docs as a subdirectory within the package
|
|
35
|
+
try:
|
|
36
|
+
help_file = resources.files('claude_code_tools') / 'docs' / 'tmux-cli-instructions.md'
|
|
37
|
+
if help_file.is_file():
|
|
38
|
+
return help_file.read_text(encoding='utf-8')
|
|
39
|
+
except:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# 2. Try accessing parent package to find docs at root level
|
|
43
|
+
try:
|
|
44
|
+
# This assumes docs/ is packaged at the same level as claude_code_tools/
|
|
45
|
+
package_root = resources.files('claude_code_tools').parent
|
|
46
|
+
help_file = package_root / 'docs' / 'tmux-cli-instructions.md'
|
|
47
|
+
if help_file.is_file():
|
|
48
|
+
return help_file.read_text(encoding='utf-8')
|
|
49
|
+
except:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
# Try pkg_resources as another fallback
|
|
53
|
+
try:
|
|
54
|
+
import pkg_resources
|
|
55
|
+
# Try different paths
|
|
56
|
+
for path in ['docs/tmux-cli-instructions.md', '../docs/tmux-cli-instructions.md']:
|
|
57
|
+
try:
|
|
58
|
+
return pkg_resources.resource_string(
|
|
59
|
+
'claude_code_tools', path
|
|
60
|
+
).decode('utf-8')
|
|
61
|
+
except:
|
|
62
|
+
continue
|
|
63
|
+
except:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# If all else fails, return a basic help message
|
|
70
|
+
return """# tmux-cli Instructions
|
|
71
|
+
|
|
72
|
+
Error: Could not load full documentation.
|
|
73
|
+
|
|
74
|
+
Basic usage:
|
|
75
|
+
- tmux-cli launch "command" - Launch a CLI application
|
|
76
|
+
- tmux-cli send "text" --pane=PANE_ID - Send input to a pane
|
|
77
|
+
- tmux-cli capture --pane=PANE_ID - Capture output from a pane
|
|
78
|
+
- tmux-cli kill --pane=PANE_ID - Kill a pane
|
|
79
|
+
- tmux-cli help - Display full help
|
|
80
|
+
|
|
81
|
+
For full documentation, see docs/tmux-cli-instructions.md in the package repository."""
|
|
15
82
|
|
|
16
83
|
|
|
17
84
|
class TmuxCLIController:
|
|
@@ -657,10 +724,6 @@ class CLI:
|
|
|
657
724
|
|
|
658
725
|
def help(self):
|
|
659
726
|
"""Display tmux-cli usage instructions."""
|
|
660
|
-
# Find the instructions file relative to this module
|
|
661
|
-
module_dir = Path(__file__).parent.parent
|
|
662
|
-
instructions_file = module_dir / "docs" / "tmux-cli-instructions.md"
|
|
663
|
-
|
|
664
727
|
# Add mode-specific header
|
|
665
728
|
mode_info = f"\n{'='*60}\n"
|
|
666
729
|
if self.mode == 'local':
|
|
@@ -670,12 +733,7 @@ class CLI:
|
|
|
670
733
|
mode_info += f"{'='*60}\n"
|
|
671
734
|
|
|
672
735
|
print(mode_info)
|
|
673
|
-
|
|
674
|
-
if instructions_file.exists():
|
|
675
|
-
print(instructions_file.read_text())
|
|
676
|
-
else:
|
|
677
|
-
print("Error: tmux-cli-instructions.md not found")
|
|
678
|
-
print(f"Expected location: {instructions_file}")
|
|
736
|
+
print(_load_help_text())
|
|
679
737
|
|
|
680
738
|
if self.mode == 'remote':
|
|
681
739
|
print("\n" + "="*60)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Remote Tmux Controller - Stub implementation
|
|
4
|
+
This is a minimal stub to prevent import errors when tmux-cli is used outside tmux.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Optional, List, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RemoteTmuxController:
|
|
12
|
+
"""Stub implementation of RemoteTmuxController to prevent import errors."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, session_name: str = "remote-cli-session"):
|
|
15
|
+
"""Initialize with session name."""
|
|
16
|
+
self.session_name = session_name
|
|
17
|
+
print(f"Warning: RemoteTmuxController is not fully implemented.")
|
|
18
|
+
print(f"Remote mode functionality is currently unavailable.")
|
|
19
|
+
print(f"Please use tmux-cli from inside a tmux session for full functionality.")
|
|
20
|
+
|
|
21
|
+
def list_panes(self) -> List[Dict[str, str]]:
|
|
22
|
+
"""Return empty list."""
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
def launch_cli(self, command: str, name: Optional[str] = None) -> Optional[str]:
|
|
26
|
+
"""Not implemented."""
|
|
27
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
28
|
+
|
|
29
|
+
def send_keys(self, text: str, pane_id: Optional[str] = None, enter: bool = True,
|
|
30
|
+
delay_enter: bool = True):
|
|
31
|
+
"""Not implemented."""
|
|
32
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
33
|
+
|
|
34
|
+
def capture_pane(self, pane_id: Optional[str] = None, lines: Optional[int] = None) -> str:
|
|
35
|
+
"""Not implemented."""
|
|
36
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
37
|
+
|
|
38
|
+
def wait_for_idle(self, pane_id: Optional[str] = None, idle_time: float = 2.0,
|
|
39
|
+
check_interval: float = 0.5, timeout: Optional[int] = None) -> bool:
|
|
40
|
+
"""Not implemented."""
|
|
41
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
42
|
+
|
|
43
|
+
def send_interrupt(self, pane_id: Optional[str] = None):
|
|
44
|
+
"""Not implemented."""
|
|
45
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
46
|
+
|
|
47
|
+
def send_escape(self, pane_id: Optional[str] = None):
|
|
48
|
+
"""Not implemented."""
|
|
49
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
50
|
+
|
|
51
|
+
def kill_window(self, window_id: Optional[str] = None):
|
|
52
|
+
"""Not implemented."""
|
|
53
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
54
|
+
|
|
55
|
+
def attach_session(self):
|
|
56
|
+
"""Not implemented."""
|
|
57
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
58
|
+
|
|
59
|
+
def cleanup_session(self):
|
|
60
|
+
"""Not implemented."""
|
|
61
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
62
|
+
|
|
63
|
+
def list_windows(self) -> List[Dict[str, str]]:
|
|
64
|
+
"""Not implemented."""
|
|
65
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
66
|
+
|
|
67
|
+
def _resolve_pane_id(self, pane: Optional[str]) -> Optional[str]:
|
|
68
|
+
"""Not implemented."""
|
|
69
|
+
raise NotImplementedError("Remote mode is not available. Please use tmux-cli from inside tmux.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-code-tools"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.10"
|
|
4
4
|
description = "Collection of tools for working with Claude Code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -23,6 +23,10 @@ requires = ["hatchling"]
|
|
|
23
23
|
build-backend = "hatchling.build"
|
|
24
24
|
|
|
25
25
|
[tool.hatch.build]
|
|
26
|
+
include = [
|
|
27
|
+
"claude_code_tools/**/*.py",
|
|
28
|
+
"docs/*.md",
|
|
29
|
+
]
|
|
26
30
|
exclude = [
|
|
27
31
|
"demos/",
|
|
28
32
|
"*.mp4",
|
|
@@ -36,7 +40,7 @@ exclude = [
|
|
|
36
40
|
|
|
37
41
|
[tool.commitizen]
|
|
38
42
|
name = "cz_conventional_commits"
|
|
39
|
-
version = "0.1.
|
|
43
|
+
version = "0.1.10"
|
|
40
44
|
tag_format = "v$version"
|
|
41
45
|
version_files = [
|
|
42
46
|
"pyproject.toml:version",
|
claude_code_tools-0.1.8/Makefile
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
.PHONY: install release patch minor major dev-install help
|
|
2
|
-
|
|
3
|
-
help:
|
|
4
|
-
@echo "Available commands:"
|
|
5
|
-
@echo " make install - Install in editable mode (for development)"
|
|
6
|
-
@echo " make dev-install - Install with dev dependencies (includes commitizen)"
|
|
7
|
-
@echo " make release - Bump patch version and install globally"
|
|
8
|
-
@echo " make patch - Bump patch version (0.0.X) and install"
|
|
9
|
-
@echo " make minor - Bump minor version (0.X.0) and install"
|
|
10
|
-
@echo " make major - Bump major version (X.0.0) and install"
|
|
11
|
-
|
|
12
|
-
install:
|
|
13
|
-
uv tool install --force -e .
|
|
14
|
-
|
|
15
|
-
dev-install:
|
|
16
|
-
uv pip install -e ".[dev]"
|
|
17
|
-
|
|
18
|
-
release: patch
|
|
19
|
-
|
|
20
|
-
patch:
|
|
21
|
-
@echo "Bumping patch version..."
|
|
22
|
-
uv run cz bump --increment PATCH --yes
|
|
23
|
-
uv tool install --force --reinstall .
|
|
24
|
-
@echo "Installation complete!"
|
|
25
|
-
|
|
26
|
-
minor:
|
|
27
|
-
@echo "Bumping minor version..."
|
|
28
|
-
uv run cz bump --increment MINOR --yes
|
|
29
|
-
uv tool install --force --reinstall .
|
|
30
|
-
@echo "Installation complete!"
|
|
31
|
-
|
|
32
|
-
major:
|
|
33
|
-
@echo "Bumping major version..."
|
|
34
|
-
uv run cz bump --increment MAJOR --yes
|
|
35
|
-
uv tool install --force --reinstall .
|
|
36
|
-
@echo "Installation complete!"
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
# Claude Code Hooks
|
|
2
|
-
|
|
3
|
-
This directory contains safety and utility hooks for Claude Code that enhance its
|
|
4
|
-
behavior and prevent dangerous operations.
|
|
5
|
-
|
|
6
|
-
## Overview
|
|
7
|
-
|
|
8
|
-
Claude Code hooks are scripts that intercept tool operations to:
|
|
9
|
-
- Prevent accidental data loss
|
|
10
|
-
- Enforce best practices
|
|
11
|
-
- Manage context size
|
|
12
|
-
- Send notifications
|
|
13
|
-
- Track operation state
|
|
14
|
-
|
|
15
|
-
## Setup
|
|
16
|
-
|
|
17
|
-
1. Copy `settings.sample.json` to `settings.json`:
|
|
18
|
-
```bash
|
|
19
|
-
cp settings.sample.json settings.json
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
2. Set the `CLAUDE_CODE_TOOLS_PATH` environment variable:
|
|
23
|
-
```bash
|
|
24
|
-
export CLAUDE_CODE_TOOLS_PATH=/path/to/claude-code-tools
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
3. Make hook scripts executable:
|
|
28
|
-
```bash
|
|
29
|
-
chmod +x hooks/*.py hooks/*.sh
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
4. Place your `settings.json` at (or add contents to) your global claude location, e.g. `~/.claude/settings.json``
|
|
33
|
-
|
|
34
|
-
## Hook Types
|
|
35
|
-
|
|
36
|
-
### Notification Hooks
|
|
37
|
-
|
|
38
|
-
Triggered for various events to send notifications.
|
|
39
|
-
|
|
40
|
-
### PreToolUse Hooks
|
|
41
|
-
|
|
42
|
-
Triggered before a tool executes. Can block operations by returning non-zero exit
|
|
43
|
-
codes with error messages.
|
|
44
|
-
|
|
45
|
-
### PostToolUse Hooks
|
|
46
|
-
|
|
47
|
-
Triggered after a tool completes. Used for cleanup and state management.
|
|
48
|
-
|
|
49
|
-
## Available Hooks
|
|
50
|
-
|
|
51
|
-
### 1. notification_hook.sh
|
|
52
|
-
|
|
53
|
-
**Type:** Notification
|
|
54
|
-
**Purpose:** Send notifications to ntfy.sh channel
|
|
55
|
-
**Behavior:**
|
|
56
|
-
- Reads JSON input and extracts the 'message' field
|
|
57
|
-
- Sends notification to ntfy.sh/cc-alerts channel
|
|
58
|
-
- Never blocks operations
|
|
59
|
-
|
|
60
|
-
**Configuration:** Update the ntfy.sh URL in the script if using a different
|
|
61
|
-
channel.
|
|
62
|
-
|
|
63
|
-
### 2. bash_hook.py
|
|
64
|
-
|
|
65
|
-
**Type:** PreToolUse (Bash)
|
|
66
|
-
**Purpose:** Unified safety checks for bash commands
|
|
67
|
-
**Blocks:**
|
|
68
|
-
- `rm` commands (enforces TRASH directory pattern)
|
|
69
|
-
- Dangerous `git add` patterns (`-A`, `--all`, `.`, `*`)
|
|
70
|
-
- Unsafe `git checkout` operations
|
|
71
|
-
- Commands that could cause data loss
|
|
72
|
-
|
|
73
|
-
**Features:**
|
|
74
|
-
- Combines multiple safety checks
|
|
75
|
-
- Provides helpful alternative suggestions
|
|
76
|
-
- Prevents accidental file deletion and git mishaps
|
|
77
|
-
|
|
78
|
-
### 3. file_size_conditional_hook.py
|
|
79
|
-
|
|
80
|
-
**Type:** PreToolUse (Read)
|
|
81
|
-
**Purpose:** Prevent reading large files that bloat context
|
|
82
|
-
**Behavior:**
|
|
83
|
-
- Main agent: Blocks files > 500 lines
|
|
84
|
-
- Sub-agents: Blocks files > 10,000 lines
|
|
85
|
-
- Binary files are always allowed
|
|
86
|
-
- Considers offset/limit parameters
|
|
87
|
-
|
|
88
|
-
**Suggestions:**
|
|
89
|
-
- Use sub-agents for large file analysis
|
|
90
|
-
- Use grep/search tools for specific content
|
|
91
|
-
- Consider external tools for very large files
|
|
92
|
-
|
|
93
|
-
### 4. pretask_subtask_flag.py & posttask_subtask_flag.py
|
|
94
|
-
|
|
95
|
-
**Type:** PreToolUse/PostToolUse (Task)
|
|
96
|
-
**Purpose:** Track sub-agent execution state
|
|
97
|
-
**Behavior:**
|
|
98
|
-
- Pre: Creates `.claude_in_subtask.flag` file
|
|
99
|
-
- Post: Removes the flag file
|
|
100
|
-
- Enables different behavior for sub-agents (like larger file limits)
|
|
101
|
-
|
|
102
|
-
### 5. grep_block_hook.py
|
|
103
|
-
|
|
104
|
-
**Type:** PreToolUse (Grep)
|
|
105
|
-
**Purpose:** Enforce use of ripgrep over grep
|
|
106
|
-
**Behavior:**
|
|
107
|
-
- Always blocks grep commands
|
|
108
|
-
- Suggests using `rg` (ripgrep) instead
|
|
109
|
-
- Ensures better performance and features
|
|
110
|
-
|
|
111
|
-
## Safety Features
|
|
112
|
-
|
|
113
|
-
### Git Safety
|
|
114
|
-
|
|
115
|
-
The bash hook includes comprehensive git safety:
|
|
116
|
-
|
|
117
|
-
**Blocked Commands:**
|
|
118
|
-
- `git add -A`, `git add --all`, `git add .`
|
|
119
|
-
- `git commit -a` without message
|
|
120
|
-
- `git checkout -f`, `git checkout .`
|
|
121
|
-
- Operations that could lose uncommitted changes
|
|
122
|
-
|
|
123
|
-
**Alternatives Suggested:**
|
|
124
|
-
- `git add -u` for modified files
|
|
125
|
-
- `git add <specific-files>` for targeted staging
|
|
126
|
-
- `git stash` before dangerous operations
|
|
127
|
-
- `git switch` for branch changes
|
|
128
|
-
|
|
129
|
-
### File Deletion Safety
|
|
130
|
-
|
|
131
|
-
**Instead of `rm`:**
|
|
132
|
-
- Move files to `TRASH/` directory
|
|
133
|
-
- Document in `TRASH-FILES.md` with reason
|
|
134
|
-
- Preserves ability to recover files
|
|
135
|
-
|
|
136
|
-
Example:
|
|
137
|
-
```bash
|
|
138
|
-
# Instead of: rm unwanted.txt
|
|
139
|
-
mv unwanted.txt TRASH/
|
|
140
|
-
echo "unwanted.txt - moved to TRASH/ - no longer needed" >> TRASH-FILES.md
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Context Management
|
|
144
|
-
|
|
145
|
-
The file size hook prevents Claude from reading huge files that would:
|
|
146
|
-
- Consume excessive context
|
|
147
|
-
- Slow down processing
|
|
148
|
-
- Potentially cause errors
|
|
149
|
-
|
|
150
|
-
## Customization
|
|
151
|
-
|
|
152
|
-
### Adding New Hooks
|
|
153
|
-
|
|
154
|
-
1. Create your hook script in the `hooks/` directory
|
|
155
|
-
2. Add it to your `settings.json`:
|
|
156
|
-
```json
|
|
157
|
-
{
|
|
158
|
-
"matcher": "ToolName",
|
|
159
|
-
"hooks": [{
|
|
160
|
-
"type": "command",
|
|
161
|
-
"command": "$CLAUDE_CODE_TOOLS_PATH/hooks/your_hook.py"
|
|
162
|
-
}]
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Hook Return Codes
|
|
167
|
-
|
|
168
|
-
- `0`: Allow operation to proceed
|
|
169
|
-
- Non-zero: Block operation (error message goes to stderr)
|
|
170
|
-
|
|
171
|
-
### Hook Input/Output
|
|
172
|
-
|
|
173
|
-
Hooks receive:
|
|
174
|
-
- Tool parameters as JSON on stdin
|
|
175
|
-
- Environment variables with context
|
|
176
|
-
|
|
177
|
-
Hooks output:
|
|
178
|
-
- Approval/rejection via exit code
|
|
179
|
-
- Error messages to stderr
|
|
180
|
-
- Logs to stdout (not shown to user)
|
|
181
|
-
|
|
182
|
-
## Best Practices
|
|
183
|
-
|
|
184
|
-
1. **Make hooks fast** - They run synchronously before operations
|
|
185
|
-
2. **Provide helpful errors** - Explain why operations are blocked
|
|
186
|
-
3. **Suggest alternatives** - Help users accomplish their goals safely
|
|
187
|
-
4. **Log for debugging** - Use stdout for diagnostic information
|
|
188
|
-
5. **Test thoroughly** - Hooks can significantly impact Claude's behavior
|
|
189
|
-
|
|
190
|
-
## Troubleshooting
|
|
191
|
-
|
|
192
|
-
### Hooks not triggering
|
|
193
|
-
|
|
194
|
-
- Verify `settings.json` is in the correct location
|
|
195
|
-
- Check file permissions (`chmod +x`)
|
|
196
|
-
- Ensure paths use `$CLAUDE_CODE_TOOLS_PATH`
|
|
197
|
-
- Test with `echo` statements to debug
|
|
198
|
-
|
|
199
|
-
### Operations being blocked unexpectedly
|
|
200
|
-
|
|
201
|
-
- Check hook logic for edge cases
|
|
202
|
-
- Review blocking conditions
|
|
203
|
-
- Add logging to understand decisions
|
|
204
|
-
- Consider making hooks more permissive for sub-agents
|
|
205
|
-
|
|
206
|
-
### Performance issues
|
|
207
|
-
|
|
208
|
-
- Hooks run synchronously - keep them fast
|
|
209
|
-
- Avoid network calls in hooks
|
|
210
|
-
- Cache results when possible
|
|
211
|
-
- Consider async notifications post-operation
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Unified Bash hook that combines all bash command safety checks.
|
|
4
|
-
This ensures that if ANY check wants to block, the command is blocked.
|
|
5
|
-
"""
|
|
6
|
-
import json
|
|
7
|
-
import sys
|
|
8
|
-
import os
|
|
9
|
-
|
|
10
|
-
# Add hooks directory to Python path so we can import the other modules
|
|
11
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
12
|
-
|
|
13
|
-
# Import check functions from other hooks
|
|
14
|
-
from git_add_block_hook import check_git_add_command
|
|
15
|
-
from git_checkout_safety_hook import check_git_checkout_command
|
|
16
|
-
from rm_block_hook import check_rm_command
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def main():
|
|
20
|
-
data = json.load(sys.stdin)
|
|
21
|
-
|
|
22
|
-
# Check if this is a Bash tool call
|
|
23
|
-
tool_name = data.get("tool_name")
|
|
24
|
-
if tool_name != "Bash":
|
|
25
|
-
print(json.dumps({"decision": "approve"}))
|
|
26
|
-
sys.exit(0)
|
|
27
|
-
|
|
28
|
-
# Get the command being executed
|
|
29
|
-
command = data.get("tool_input", {}).get("command", "")
|
|
30
|
-
|
|
31
|
-
# Run all checks - collect all blocking reasons
|
|
32
|
-
checks = [
|
|
33
|
-
check_rm_command,
|
|
34
|
-
check_git_add_command,
|
|
35
|
-
check_git_checkout_command,
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
blocking_reasons = []
|
|
39
|
-
|
|
40
|
-
for check_func in checks:
|
|
41
|
-
should_block, reason = check_func(command)
|
|
42
|
-
if should_block:
|
|
43
|
-
blocking_reasons.append(reason)
|
|
44
|
-
|
|
45
|
-
# If any check wants to block, block the command
|
|
46
|
-
if blocking_reasons:
|
|
47
|
-
# If multiple checks want to block, combine the reasons
|
|
48
|
-
if len(blocking_reasons) == 1:
|
|
49
|
-
combined_reason = blocking_reasons[0]
|
|
50
|
-
else:
|
|
51
|
-
combined_reason = "Multiple safety checks failed:\n\n"
|
|
52
|
-
for i, reason in enumerate(blocking_reasons, 1):
|
|
53
|
-
combined_reason += f"{i}. {reason}\n\n"
|
|
54
|
-
|
|
55
|
-
print(json.dumps({
|
|
56
|
-
"decision": "block",
|
|
57
|
-
"reason": combined_reason
|
|
58
|
-
}, ensure_ascii=False))
|
|
59
|
-
else:
|
|
60
|
-
print(json.dumps({"decision": "approve"}))
|
|
61
|
-
|
|
62
|
-
sys.exit(0)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if __name__ == "__main__":
|
|
66
|
-
main()
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import os
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
import subprocess
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
data = json.load(sys.stdin)
|
|
9
|
-
|
|
10
|
-
# Check if we're in a subtask
|
|
11
|
-
flag_file = '.claude_in_subtask.flag'
|
|
12
|
-
is_main_agent = not os.path.exists(flag_file)
|
|
13
|
-
# if os.path.exists(flag_file):
|
|
14
|
-
# print(json.dumps({"decision": "approve"}))
|
|
15
|
-
# sys.exit(0)
|
|
16
|
-
|
|
17
|
-
# Check file size
|
|
18
|
-
file_path = data.get("tool_input", {}).get("file_path")
|
|
19
|
-
offset = data.get("tool_input", {}).get("offset",0)
|
|
20
|
-
limit = data.get("tool_input", {}).get("limit",0) # 0 if absent
|
|
21
|
-
|
|
22
|
-
if file_path and os.path.exists(file_path):
|
|
23
|
-
# Check if this is a binary file by examining its content
|
|
24
|
-
def is_binary_file(filepath):
|
|
25
|
-
"""Check if a file is binary by looking for null bytes in first chunk"""
|
|
26
|
-
try:
|
|
27
|
-
with open(filepath, 'rb') as f:
|
|
28
|
-
# Read first 8192 bytes (or less if file is smaller)
|
|
29
|
-
chunk = f.read(8192)
|
|
30
|
-
if not chunk: # Empty file
|
|
31
|
-
return False
|
|
32
|
-
|
|
33
|
-
# Files with null bytes are likely binary
|
|
34
|
-
if b'\x00' in chunk:
|
|
35
|
-
return True
|
|
36
|
-
|
|
37
|
-
# Try to decode as UTF-8
|
|
38
|
-
try:
|
|
39
|
-
chunk.decode('utf-8')
|
|
40
|
-
return False
|
|
41
|
-
except UnicodeDecodeError:
|
|
42
|
-
return True
|
|
43
|
-
except Exception:
|
|
44
|
-
# If we can't read the file, assume it's binary to be safe
|
|
45
|
-
return True
|
|
46
|
-
|
|
47
|
-
# Skip line count check for binary files
|
|
48
|
-
if is_binary_file(file_path):
|
|
49
|
-
print(json.dumps({"decision": "approve"}))
|
|
50
|
-
sys.exit(0)
|
|
51
|
-
|
|
52
|
-
line_count = int(subprocess.check_output(['wc', '-l', file_path]).split()[0])
|
|
53
|
-
|
|
54
|
-
# Compute effective number of lines to be read
|
|
55
|
-
if limit > 0:
|
|
56
|
-
# If limit is specified, we read from offset to offset+limit
|
|
57
|
-
effective_lines = min(limit, max(0, line_count - offset))
|
|
58
|
-
else:
|
|
59
|
-
# If no limit, we read from offset to end of file
|
|
60
|
-
effective_lines = max(0, line_count - offset)
|
|
61
|
-
|
|
62
|
-
if is_main_agent and line_count > 500:
|
|
63
|
-
print(json.dumps({
|
|
64
|
-
"decision": "block",
|
|
65
|
-
"reason": f"""
|
|
66
|
-
I see you are trying to read a file with {line_count} lines,
|
|
67
|
-
or a part of it.
|
|
68
|
-
Please delegate the analysis to a SUB-AGENT using your Task tool,
|
|
69
|
-
so you don't bloat your context with the file content!
|
|
70
|
-
""",
|
|
71
|
-
}))
|
|
72
|
-
sys.exit(0)
|
|
73
|
-
elif (not is_main_agent) and line_count > 10_000:
|
|
74
|
-
# use gemini-cli to delegate the analysis
|
|
75
|
-
print(json.dumps({
|
|
76
|
-
"decision": "block",
|
|
77
|
-
"reason": f"""
|
|
78
|
-
File too large ({line_count} lines), please use the Gemini CLI
|
|
79
|
-
bash command to delegate the analysis to Gemini since it has
|
|
80
|
-
a 1M-token context window! This will help you avoid bloating
|
|
81
|
-
your context.
|
|
82
|
-
|
|
83
|
-
You can use Gemini CLI as in these EXAMPLES:
|
|
84
|
-
|
|
85
|
-
`gemini -p "@src/somefile.py tell me at which line the definition of
|
|
86
|
-
the function 'my_function' is located"
|
|
87
|
-
|
|
88
|
-
`gemini -p "@package.json @src/index.js Analyze the dependencies used in the code"
|
|
89
|
-
|
|
90
|
-
See further guidelines in claude-mds/use-gemini-cli.md
|
|
91
|
-
""",
|
|
92
|
-
}))
|
|
93
|
-
sys.exit(0)
|
|
94
|
-
|
|
95
|
-
print(json.dumps({"decision": "approve"}))
|
|
96
|
-
sys.exit(0)
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
def check_git_add_command(command):
|
|
5
|
-
"""
|
|
6
|
-
Check if a git add command contains dangerous patterns.
|
|
7
|
-
Returns tuple: (should_block: bool, reason: str or None)
|
|
8
|
-
"""
|
|
9
|
-
# Normalize the command - handle multiple spaces, tabs, etc.
|
|
10
|
-
normalized_cmd = ' '.join(command.strip().split())
|
|
11
|
-
|
|
12
|
-
# Pattern to match git add with problematic flags
|
|
13
|
-
# This catches: git add -A, git add --all, git add -a, git add ., and combined flags
|
|
14
|
-
git_add_pattern = re.compile(
|
|
15
|
-
r'^git\s+add\s+('
|
|
16
|
-
r'-[a-zA-Z]*[Aa][a-zA-Z]*|' # Flags containing 'A' or 'a' (e.g., -A, -a, -fA, -Af)
|
|
17
|
-
r'--all|' # Long form --all
|
|
18
|
-
r'\.|' # git add . (adds everything in current dir)
|
|
19
|
-
r'\*' # git add * (shell expansion of all files)
|
|
20
|
-
r')', re.IGNORECASE
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
if git_add_pattern.search(normalized_cmd):
|
|
24
|
-
reason = """DO NOT use 'git add -A', 'git add -a', 'git add --all', 'git add .' or 'git add *' as they add ALL files!
|
|
25
|
-
|
|
26
|
-
Instead, use one of these commands:
|
|
27
|
-
- 'git add -u' to stage all modified and deleted files (but not untracked)
|
|
28
|
-
- 'git add <specific-files>' to stage specific files you want
|
|
29
|
-
- 'gsuno' (alias for 'git status -uno') to see modified files only
|
|
30
|
-
- 'gcam "message"' to commit all modified files with a message
|
|
31
|
-
|
|
32
|
-
This restriction prevents accidentally staging unwanted files."""
|
|
33
|
-
return True, reason
|
|
34
|
-
|
|
35
|
-
# Also check for git commit -a without -m (which would open an editor)
|
|
36
|
-
# Check if command has -a flag but no -m flag
|
|
37
|
-
if re.search(r'^git\s+commit\s+', normalized_cmd):
|
|
38
|
-
has_a_flag = re.search(r'-[a-zA-Z]*a[a-zA-Z]*', normalized_cmd)
|
|
39
|
-
has_m_flag = re.search(r'-[a-zA-Z]*m[a-zA-Z]*', normalized_cmd)
|
|
40
|
-
if has_a_flag and not has_m_flag:
|
|
41
|
-
reason = """Avoid 'git commit -a' without a message flag. Use 'gcam "message"' instead, which is an alias for 'git commit -a -m'."""
|
|
42
|
-
return True, reason
|
|
43
|
-
|
|
44
|
-
return False, None
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# If run as a standalone script
|
|
48
|
-
if __name__ == "__main__":
|
|
49
|
-
import json
|
|
50
|
-
import sys
|
|
51
|
-
|
|
52
|
-
data = json.load(sys.stdin)
|
|
53
|
-
|
|
54
|
-
# Check if this is a Bash tool call
|
|
55
|
-
tool_name = data.get("tool_name")
|
|
56
|
-
if tool_name != "Bash":
|
|
57
|
-
print(json.dumps({"decision": "approve"}))
|
|
58
|
-
sys.exit(0)
|
|
59
|
-
|
|
60
|
-
# Get the command being executed
|
|
61
|
-
command = data.get("tool_input", {}).get("command", "")
|
|
62
|
-
|
|
63
|
-
should_block, reason = check_git_add_command(command)
|
|
64
|
-
|
|
65
|
-
if should_block:
|
|
66
|
-
print(json.dumps({
|
|
67
|
-
"decision": "block",
|
|
68
|
-
"reason": reason
|
|
69
|
-
}))
|
|
70
|
-
else:
|
|
71
|
-
print(json.dumps({"decision": "approve"}))
|
|
72
|
-
|
|
73
|
-
sys.exit(0)
|