claude-code-tools 0.1.17__tar.gz → 0.1.18__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.

Files changed (19) hide show
  1. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/PKG-INFO +19 -13
  2. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/README.md +18 -12
  3. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/claude_code_tools/__init__.py +1 -1
  4. claude_code_tools-0.1.18/claude_code_tools/tmux_remote_controller.py +229 -0
  5. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/pyproject.toml +2 -2
  6. claude_code_tools-0.1.17/claude_code_tools/tmux_remote_controller.py +0 -69
  7. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/.gitignore +0 -0
  8. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/LICENSE +0 -0
  9. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/claude_code_tools/dotenv_vault.py +0 -0
  10. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/claude_code_tools/env_safe.py +0 -0
  11. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/claude_code_tools/find_claude_session.py +0 -0
  12. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/claude_code_tools/tmux_cli_controller.py +0 -0
  13. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/claude-code-chutes.md +0 -0
  14. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/claude-code-tmux-tutorials.md +0 -0
  15. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/dot-zshrc.md +0 -0
  16. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/find-claude-session.md +0 -0
  17. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/reddit-post.md +0 -0
  18. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/tmux-cli-instructions.md +0 -0
  19. {claude_code_tools-0.1.17 → claude_code_tools-0.1.18}/docs/vault-documentation.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 0.1.17
3
+ Version: 0.1.18
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -213,7 +213,7 @@ env-safe --help # See all options
213
213
 
214
214
  ### Why env-safe?
215
215
 
216
- When Claude Code attempts to read .env files directly (via cat, grep, etc.), safety hooks block the operation to prevent accidental exposure of API keys and secrets. The `env-safe` command provides a secure alternative that lets Claude Code inspect environment configuration without security risks.
216
+ Claude Code is completely blocked from directly accessing .env files - no reading, writing, or editing allowed. This prevents both accidental exposure of API keys and unintended modifications. The `env-safe` command provides the only approved way for Claude Code to inspect environment configuration safely, while any modifications must be done manually outside of Claude Code.
217
217
 
218
218
  ## 🛡️ Claude Code Safety Hooks
219
219
 
@@ -226,8 +226,8 @@ Code's behavior and prevent dangerous operations.
226
226
  pattern
227
227
  - **Git Safety** - Prevents dangerous `git add -A`, unsafe checkouts, and
228
228
  accidental data loss
229
- - **Environment Security** - Blocks direct .env file access, suggests `env-safe`
230
- command instead
229
+ - **Environment Security** - Blocks all .env file operations (read/write/edit),
230
+ suggests `env-safe` command for safe inspection
231
231
  - **Context Management** - Blocks reading files >500 lines to prevent context
232
232
  bloat
233
233
  - **Command Enhancement** - Enforces ripgrep (`rg`) over grep for better
@@ -235,20 +235,26 @@ Code's behavior and prevent dangerous operations.
235
235
 
236
236
  ### Quick Setup
237
237
 
238
- 1. Copy the sample hooks configuration:
239
- ```bash
240
- cp hooks/settings.sample.json hooks/settings.json
241
- export CLAUDE_CODE_TOOLS_PATH=/path/to/claude-code-tools
242
- ```
243
-
244
- 2. Reference in your Claude Code settings or use `--hooks` flag:
245
- ```bash
246
- claude --hooks /path/to/hooks/settings.json
238
+ 1. Copy the hooks configuration from `hooks/settings.sample.json`
239
+
240
+ 2. Add the hooks to your global Claude settings at `~/.claude/settings.json`:
241
+ - If the file doesn't exist, create it
242
+ - Copy the "hooks" section from settings.sample.json
243
+ - Replace `/path/to/claude-code-tools` with your actual path to this repository
244
+
245
+ Example ~/.claude/settings.json:
246
+ ```json
247
+ {
248
+ "hooks": {
249
+ // ... hooks configuration from settings.sample.json ...
250
+ }
251
+ }
247
252
  ```
248
253
 
249
254
  ### Available Hooks
250
255
 
251
256
  - `bash_hook.py` - Comprehensive bash command safety checks
257
+ - `env_file_protection_hook.py` - Blocks all .env file operations
252
258
  - `file_size_conditional_hook.py` - Prevents reading huge files
253
259
  - `grep_block_hook.py` - Enforces ripgrep usage
254
260
  - `notification_hook.sh` - Sends ntfy.sh notifications
@@ -200,7 +200,7 @@ env-safe --help # See all options
200
200
 
201
201
  ### Why env-safe?
202
202
 
203
- When Claude Code attempts to read .env files directly (via cat, grep, etc.), safety hooks block the operation to prevent accidental exposure of API keys and secrets. The `env-safe` command provides a secure alternative that lets Claude Code inspect environment configuration without security risks.
203
+ Claude Code is completely blocked from directly accessing .env files - no reading, writing, or editing allowed. This prevents both accidental exposure of API keys and unintended modifications. The `env-safe` command provides the only approved way for Claude Code to inspect environment configuration safely, while any modifications must be done manually outside of Claude Code.
204
204
 
205
205
  ## 🛡️ Claude Code Safety Hooks
206
206
 
@@ -213,8 +213,8 @@ Code's behavior and prevent dangerous operations.
213
213
  pattern
214
214
  - **Git Safety** - Prevents dangerous `git add -A`, unsafe checkouts, and
215
215
  accidental data loss
216
- - **Environment Security** - Blocks direct .env file access, suggests `env-safe`
217
- command instead
216
+ - **Environment Security** - Blocks all .env file operations (read/write/edit),
217
+ suggests `env-safe` command for safe inspection
218
218
  - **Context Management** - Blocks reading files >500 lines to prevent context
219
219
  bloat
220
220
  - **Command Enhancement** - Enforces ripgrep (`rg`) over grep for better
@@ -222,20 +222,26 @@ Code's behavior and prevent dangerous operations.
222
222
 
223
223
  ### Quick Setup
224
224
 
225
- 1. Copy the sample hooks configuration:
226
- ```bash
227
- cp hooks/settings.sample.json hooks/settings.json
228
- export CLAUDE_CODE_TOOLS_PATH=/path/to/claude-code-tools
229
- ```
230
-
231
- 2. Reference in your Claude Code settings or use `--hooks` flag:
232
- ```bash
233
- claude --hooks /path/to/hooks/settings.json
225
+ 1. Copy the hooks configuration from `hooks/settings.sample.json`
226
+
227
+ 2. Add the hooks to your global Claude settings at `~/.claude/settings.json`:
228
+ - If the file doesn't exist, create it
229
+ - Copy the "hooks" section from settings.sample.json
230
+ - Replace `/path/to/claude-code-tools` with your actual path to this repository
231
+
232
+ Example ~/.claude/settings.json:
233
+ ```json
234
+ {
235
+ "hooks": {
236
+ // ... hooks configuration from settings.sample.json ...
237
+ }
238
+ }
234
239
  ```
235
240
 
236
241
  ### Available Hooks
237
242
 
238
243
  - `bash_hook.py` - Comprehensive bash command safety checks
244
+ - `env_file_protection_hook.py` - Blocks all .env file operations
239
245
  - `file_size_conditional_hook.py` - Prevents reading huge files
240
246
  - `grep_block_hook.py` - Enforces ripgrep usage
241
247
  - `notification_hook.sh` - Sends ntfy.sh notifications
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "0.1.17"
3
+ __version__ = "0.1.18"
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Remote Tmux Controller
4
+
5
+ Enables tmux-cli to work when run outside of tmux by:
6
+ - Auto-creating a detached tmux session on first use
7
+ - Managing commands in separate tmux windows (not panes)
8
+ - Providing an API compatible with the local (pane) controller
9
+ """
10
+
11
+ import subprocess
12
+ import time
13
+ import hashlib
14
+ from typing import Optional, List, Dict, Tuple, Union
15
+
16
+
17
+ class RemoteTmuxController:
18
+ """Remote controller that manages a dedicated tmux session and windows."""
19
+
20
+ def __init__(self, session_name: str = "remote-cli-session"):
21
+ """Initialize with session name and ensure the session exists."""
22
+ self.session_name = session_name
23
+ self.target_window: Optional[str] = None # e.g., "session:0" (active pane in that window)
24
+ print(f"Note: tmux-cli is running outside tmux. Managing windows in session '{session_name}'.")
25
+ print("For better integration, consider running from inside a tmux session.")
26
+ print("Use 'tmux-cli attach' to view the remote session.")
27
+ self._ensure_session()
28
+
29
+ # ----------------------------
30
+ # Internal utilities
31
+ # ----------------------------
32
+ def _run_tmux(self, args: List[str]) -> Tuple[str, int]:
33
+ result = subprocess.run(
34
+ ['tmux'] + args,
35
+ capture_output=True,
36
+ text=True
37
+ )
38
+ return result.stdout.strip(), result.returncode
39
+
40
+ def _ensure_session(self) -> None:
41
+ """Create the session if it doesn't exist (detached)."""
42
+ _, code = self._run_tmux(['has-session', '-t', self.session_name])
43
+ if code != 0:
44
+ # Create a detached session using user's default shell
45
+ # Return the session name just to force creation
46
+ self._run_tmux([
47
+ 'new-session', '-d', '-s', self.session_name, '-P', '-F', '#{session_name}'
48
+ ])
49
+ # Remember first window as default target
50
+ self.target_window = f"{self.session_name}:0"
51
+ else:
52
+ # If already exists and we don't have a target, set to active window
53
+ if not self.target_window:
54
+ win, code2 = self._run_tmux(['display-message', '-p', '-t', self.session_name, '#{session_name}:#{window_index}'])
55
+ if code2 == 0 and win:
56
+ self.target_window = win
57
+
58
+ def _window_target(self, pane: Optional[str]) -> str:
59
+ """Resolve user-provided pane/window hint to a tmux target.
60
+ Accepts:
61
+ - None -> use last target window if set else active window in session
62
+ - digits (e.g., "1") -> session:index
63
+ - full tmux target (e.g., "name:1" or "name:1.0" or "%12") -> pass-through
64
+ """
65
+ self._ensure_session()
66
+ if pane is None:
67
+ if self.target_window:
68
+ return self.target_window
69
+ # Fallback to active window in session
70
+ win, code = self._run_tmux(['display-message', '-p', '-t', self.session_name, '#{session_name}:#{window_index}'])
71
+ if code == 0 and win:
72
+ self.target_window = win
73
+ return win
74
+ # Final fallback: session:0
75
+ return f"{self.session_name}:0"
76
+ # If user supplied a simple index
77
+ if isinstance(pane, str) and pane.isdigit():
78
+ return f"{self.session_name}:{pane}"
79
+ # Otherwise assume user provided a pane/window target or pane id
80
+ return pane
81
+
82
+ def _active_pane_in_window(self, window_target: str) -> str:
83
+ """Return a target that tmux can use to address the active pane of a window.
84
+ For tmux commands that accept pane targets, a window target resolves to its
85
+ active pane, so we can pass the window target directly.
86
+ Still, normalize to make intent clear.
87
+ """
88
+ return window_target
89
+
90
+ def list_panes(self) -> List[Dict[str, str]]:
91
+ """In remote mode, list windows in the managed session.
92
+ Returns a list shaped similarly to local list_panes, with keys:
93
+ id (window target), index, title (window name), active (bool), size (N/A)
94
+ """
95
+ self._ensure_session()
96
+ out, code = self._run_tmux([
97
+ 'list-windows', '-t', self.session_name,
98
+ '-F', '#{window_index}|#{window_name}|#{window_active}|#{window_width}x#{window_height}'
99
+ ])
100
+ if code != 0 or not out:
101
+ return []
102
+ windows: List[Dict[str, str]] = []
103
+ for line in out.split('\n'):
104
+ if not line:
105
+ continue
106
+ idx, name, active, size = line.split('|')
107
+ windows.append({
108
+ 'id': f"{self.session_name}:{idx}",
109
+ 'index': idx,
110
+ 'title': name,
111
+ 'active': active == '1',
112
+ 'size': size
113
+ })
114
+ return windows
115
+
116
+ def launch_cli(self, command: str, name: Optional[str] = None) -> Optional[str]:
117
+ """Launch a command in a new window within the managed session.
118
+ Returns the window target (e.g., "session:1").
119
+ """
120
+ self._ensure_session()
121
+ args = ['new-window', '-t', self.session_name, '-P', '-F', '#{session_name}:#{window_index}']
122
+ if name:
123
+ args.extend(['-n', name])
124
+ if command:
125
+ args.append(command)
126
+ out, code = self._run_tmux(args)
127
+ if code == 0 and out:
128
+ self.target_window = out
129
+ return out
130
+ return None
131
+
132
+ def send_keys(self, text: str, pane_id: Optional[str] = None, enter: bool = True,
133
+ delay_enter: Union[bool, float] = True):
134
+ """Send keys to the active pane of a given window (or last target)."""
135
+ if not text:
136
+ return
137
+ target = self._active_pane_in_window(self._window_target(pane_id))
138
+ if enter and delay_enter:
139
+ # First send text (no Enter)
140
+ self._run_tmux(['send-keys', '-t', target, text])
141
+ # Delay
142
+ delay = 1.0 if isinstance(delay_enter, bool) else float(delay_enter)
143
+ time.sleep(delay)
144
+ # Then Enter
145
+ self._run_tmux(['send-keys', '-t', target, 'Enter'])
146
+ else:
147
+ args = ['send-keys', '-t', target, text]
148
+ if enter:
149
+ args.append('Enter')
150
+ self._run_tmux(args)
151
+
152
+ def capture_pane(self, pane_id: Optional[str] = None, lines: Optional[int] = None) -> str:
153
+ """Capture output from the active pane of a window."""
154
+ target = self._active_pane_in_window(self._window_target(pane_id))
155
+ args = ['capture-pane', '-t', target, '-p']
156
+ if lines:
157
+ args.extend(['-S', f'-{lines}'])
158
+ out, _ = self._run_tmux(args)
159
+ return out
160
+
161
+ def wait_for_idle(self, pane_id: Optional[str] = None, idle_time: float = 2.0,
162
+ check_interval: float = 0.5, timeout: Optional[int] = None) -> bool:
163
+ """Wait until captured output is unchanged for idle_time seconds."""
164
+ target = self._active_pane_in_window(self._window_target(pane_id))
165
+ start_time = time.time()
166
+ last_change = time.time()
167
+ last_hash = ""
168
+ while True:
169
+ if timeout is not None and (time.time() - start_time) > timeout:
170
+ return False
171
+ content, _ = self._run_tmux(['capture-pane', '-t', target, '-p'])
172
+ h = hashlib.md5(content.encode()).hexdigest()
173
+ if h != last_hash:
174
+ last_hash = h
175
+ last_change = time.time()
176
+ else:
177
+ if (time.time() - last_change) >= idle_time:
178
+ return True
179
+ time.sleep(check_interval)
180
+
181
+ def send_interrupt(self, pane_id: Optional[str] = None):
182
+ target = self._active_pane_in_window(self._window_target(pane_id))
183
+ self._run_tmux(['send-keys', '-t', target, 'C-c'])
184
+
185
+ def send_escape(self, pane_id: Optional[str] = None):
186
+ target = self._active_pane_in_window(self._window_target(pane_id))
187
+ self._run_tmux(['send-keys', '-t', target, 'Escape'])
188
+
189
+ def kill_window(self, window_id: Optional[str] = None):
190
+ target = self._window_target(window_id)
191
+ # Ensure the target refers to a window (not a %pane id)
192
+ # If user passed a pane id like %12, tmux can still resolve to its window
193
+ self._run_tmux(['kill-window', '-t', target])
194
+ if self.target_window == target:
195
+ self.target_window = None
196
+
197
+ def attach_session(self):
198
+ self._ensure_session()
199
+ # Attach will replace the current terminal view until the user detaches
200
+ subprocess.run(['tmux', 'attach-session', '-t', self.session_name])
201
+
202
+ def cleanup_session(self):
203
+ self._run_tmux(['kill-session', '-t', self.session_name])
204
+ self.target_window = None
205
+
206
+ def list_windows(self) -> List[Dict[str, str]]:
207
+ """List all windows in the managed session with basic info."""
208
+ self._ensure_session()
209
+ out, code = self._run_tmux(['list-windows', '-t', self.session_name, '-F', '#{window_index}|#{window_name}|#{window_active}'])
210
+ if code != 0 or not out:
211
+ return []
212
+ windows: List[Dict[str, str]] = []
213
+ for line in out.split('\n'):
214
+ if not line:
215
+ continue
216
+ idx, name, active = line.split('|')
217
+ # Try to get active pane id for each window (best effort)
218
+ pane_out, _ = self._run_tmux(['display-message', '-p', '-t', f'{self.session_name}:{idx}', '#{pane_id}'])
219
+ windows.append({
220
+ 'index': idx,
221
+ 'name': name,
222
+ 'active': active == '1',
223
+ 'pane_id': pane_out or ''
224
+ })
225
+ return windows
226
+
227
+ def _resolve_pane_id(self, pane: Optional[str]) -> Optional[str]:
228
+ """Resolve user-provided identifier to a tmux target string for remote ops."""
229
+ return self._window_target(pane)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "0.1.17"
3
+ version = "0.1.18"
4
4
  description = "Collection of tools for working with Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -41,7 +41,7 @@ exclude = [
41
41
 
42
42
  [tool.commitizen]
43
43
  name = "cz_conventional_commits"
44
- version = "0.1.17"
44
+ version = "0.1.18"
45
45
  tag_format = "v$version"
46
46
  version_files = [
47
47
  "pyproject.toml:version",
@@ -1,69 +0,0 @@
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.")