claude-code-tools 0.1.13__tar.gz → 0.1.17__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 (18) hide show
  1. claude_code_tools-0.1.17/LICENSE +21 -0
  2. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/PKG-INFO +36 -1
  3. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/README.md +34 -0
  4. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/claude_code_tools/__init__.py +1 -1
  5. claude_code_tools-0.1.17/claude_code_tools/env_safe.py +235 -0
  6. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/claude_code_tools/find_claude_session.py +50 -21
  7. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/claude_code_tools/tmux_cli_controller.py +20 -0
  8. claude_code_tools-0.1.17/docs/dot-zshrc.md +307 -0
  9. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/pyproject.toml +3 -2
  10. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/.gitignore +0 -0
  11. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/claude_code_tools/dotenv_vault.py +0 -0
  12. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/claude_code_tools/tmux_remote_controller.py +0 -0
  13. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/docs/claude-code-chutes.md +0 -0
  14. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/docs/claude-code-tmux-tutorials.md +0 -0
  15. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/docs/find-claude-session.md +0 -0
  16. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/docs/reddit-post.md +0 -0
  17. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/docs/tmux-cli-instructions.md +0 -0
  18. {claude_code_tools-0.1.13 → claude_code_tools-0.1.17}/docs/vault-documentation.md +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Prasad Chalasani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 0.1.13
3
+ Version: 0.1.17
4
4
  Summary: Collection of tools for working with Claude Code
5
+ License-File: LICENSE
5
6
  Requires-Python: >=3.11
6
7
  Requires-Dist: click>=8.0.0
7
8
  Requires-Dist: fire>=0.5.0
@@ -62,6 +63,7 @@ This gives you:
62
63
  - `tmux-cli` - The interactive CLI controller we just covered
63
64
  - `find-claude-session` - Search and resume Claude Code sessions by keywords
64
65
  - `vault` - Encrypted backup for your .env files
66
+ - `env-safe` - Safely inspect .env files without exposing values
65
67
 
66
68
  ## 🎮 tmux-cli Deep Dive
67
69
 
@@ -106,6 +108,12 @@ Example uses:
106
108
  - Launch web apps and test them with browser automation MCP tools like Puppeteer
107
109
  ```
108
110
 
111
+ Incidentally, installing the Puppeteer MCP tool is easy:
112
+
113
+ ```bash
114
+ claude mcp add puppeteer -- npx -y @modelcontextprotocol/server-puppeteer
115
+ ```
116
+
109
117
  For detailed instructions, see [docs/tmux-cli-instructions.md](docs/tmux-cli-instructions.md).
110
118
 
111
119
  ## 🔍 find-claude-session
@@ -182,6 +190,31 @@ vault status # Check sync status for current project
182
190
 
183
191
  For detailed documentation, see [docs/vault-documentation.md](docs/vault-documentation.md).
184
192
 
193
+ ## 🔍 env-safe
194
+
195
+ Safely inspect .env files without exposing sensitive values. Designed for Claude Code and other automated tools that need to work with environment files without accidentally leaking secrets.
196
+
197
+ ```bash
198
+ env-safe list # List all environment variable keys
199
+ env-safe list --status # Show keys with defined/empty status
200
+ env-safe check API_KEY # Check if a specific key exists
201
+ env-safe count # Count total, defined, and empty variables
202
+ env-safe validate # Validate .env file syntax
203
+ env-safe --help # See all options
204
+ ```
205
+
206
+ ### Key Features
207
+
208
+ - **No Value Exposure** - Never displays actual environment values
209
+ - **Safe Inspection** - Check which keys exist without security risks
210
+ - **Syntax Validation** - Verify .env file format is correct
211
+ - **Status Checking** - See which variables are defined vs empty
212
+ - **Claude Code Integration** - Works with protection hooks to provide safe alternative
213
+
214
+ ### Why env-safe?
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.
217
+
185
218
  ## 🛡️ Claude Code Safety Hooks
186
219
 
187
220
  This repository includes a comprehensive set of safety hooks that enhance Claude
@@ -193,6 +226,8 @@ Code's behavior and prevent dangerous operations.
193
226
  pattern
194
227
  - **Git Safety** - Prevents dangerous `git add -A`, unsafe checkouts, and
195
228
  accidental data loss
229
+ - **Environment Security** - Blocks direct .env file access, suggests `env-safe`
230
+ command instead
196
231
  - **Context Management** - Blocks reading files >500 lines to prevent context
197
232
  bloat
198
233
  - **Command Enhancement** - Enforces ripgrep (`rg`) over grep for better
@@ -50,6 +50,7 @@ This gives you:
50
50
  - `tmux-cli` - The interactive CLI controller we just covered
51
51
  - `find-claude-session` - Search and resume Claude Code sessions by keywords
52
52
  - `vault` - Encrypted backup for your .env files
53
+ - `env-safe` - Safely inspect .env files without exposing values
53
54
 
54
55
  ## 🎮 tmux-cli Deep Dive
55
56
 
@@ -94,6 +95,12 @@ Example uses:
94
95
  - Launch web apps and test them with browser automation MCP tools like Puppeteer
95
96
  ```
96
97
 
98
+ Incidentally, installing the Puppeteer MCP tool is easy:
99
+
100
+ ```bash
101
+ claude mcp add puppeteer -- npx -y @modelcontextprotocol/server-puppeteer
102
+ ```
103
+
97
104
  For detailed instructions, see [docs/tmux-cli-instructions.md](docs/tmux-cli-instructions.md).
98
105
 
99
106
  ## 🔍 find-claude-session
@@ -170,6 +177,31 @@ vault status # Check sync status for current project
170
177
 
171
178
  For detailed documentation, see [docs/vault-documentation.md](docs/vault-documentation.md).
172
179
 
180
+ ## 🔍 env-safe
181
+
182
+ Safely inspect .env files without exposing sensitive values. Designed for Claude Code and other automated tools that need to work with environment files without accidentally leaking secrets.
183
+
184
+ ```bash
185
+ env-safe list # List all environment variable keys
186
+ env-safe list --status # Show keys with defined/empty status
187
+ env-safe check API_KEY # Check if a specific key exists
188
+ env-safe count # Count total, defined, and empty variables
189
+ env-safe validate # Validate .env file syntax
190
+ env-safe --help # See all options
191
+ ```
192
+
193
+ ### Key Features
194
+
195
+ - **No Value Exposure** - Never displays actual environment values
196
+ - **Safe Inspection** - Check which keys exist without security risks
197
+ - **Syntax Validation** - Verify .env file format is correct
198
+ - **Status Checking** - See which variables are defined vs empty
199
+ - **Claude Code Integration** - Works with protection hooks to provide safe alternative
200
+
201
+ ### Why env-safe?
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.
204
+
173
205
  ## 🛡️ Claude Code Safety Hooks
174
206
 
175
207
  This repository includes a comprehensive set of safety hooks that enhance Claude
@@ -181,6 +213,8 @@ Code's behavior and prevent dangerous operations.
181
213
  pattern
182
214
  - **Git Safety** - Prevents dangerous `git add -A`, unsafe checkouts, and
183
215
  accidental data loss
216
+ - **Environment Security** - Blocks direct .env file access, suggests `env-safe`
217
+ command instead
184
218
  - **Context Management** - Blocks reading files >500 lines to prevent context
185
219
  bloat
186
220
  - **Command Enhancement** - Enforces ripgrep (`rg`) over grep for better
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "0.1.13"
3
+ __version__ = "0.1.17"
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ env-safe: A safe way to inspect .env files without exposing sensitive values.
4
+
5
+ This tool allows Claude Code and other automated tools to:
6
+ - List environment variable keys without showing values
7
+ - Check if specific keys exist
8
+ - Count the number of variables defined
9
+ - Validate .env file syntax
10
+
11
+ It specifically avoids displaying actual values to prevent accidental exposure
12
+ of secrets, API keys, and other sensitive information.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import argparse
18
+ from pathlib import Path
19
+ from typing import List, Tuple, Optional
20
+ import re
21
+
22
+
23
+ def parse_env_file(filepath: Path) -> List[Tuple[str, bool]]:
24
+ """
25
+ Parse a .env file and extract variable names.
26
+
27
+ Returns:
28
+ List of tuples: (variable_name, has_value)
29
+ """
30
+ if not filepath.exists():
31
+ raise FileNotFoundError(f"File not found: {filepath}")
32
+
33
+ variables = []
34
+
35
+ with open(filepath, 'r', encoding='utf-8') as f:
36
+ for line_num, line in enumerate(f, 1):
37
+ # Skip empty lines and comments
38
+ line = line.strip()
39
+ if not line or line.startswith('#'):
40
+ continue
41
+
42
+ # Match KEY=value pattern
43
+ match = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$', line)
44
+ if match:
45
+ key = match.group(1)
46
+ value = match.group(2).strip()
47
+ has_value = bool(value and value != '""' and value != "''")
48
+ variables.append((key, has_value))
49
+ elif '=' in line:
50
+ # Malformed line - has = but doesn't match pattern
51
+ print(f"Warning: Line {line_num} appears malformed: {line[:50]}...",
52
+ file=sys.stderr)
53
+
54
+ return variables
55
+
56
+
57
+ def list_keys(filepath: Path, show_status: bool = False) -> None:
58
+ """List all environment variable keys in the file."""
59
+ try:
60
+ variables = parse_env_file(filepath)
61
+
62
+ if not variables:
63
+ print("No environment variables found in file.")
64
+ return
65
+
66
+ if show_status:
67
+ print(f"{'KEY':<30} {'STATUS':<10}")
68
+ print("-" * 40)
69
+ for key, has_value in sorted(variables):
70
+ status = "defined" if has_value else "empty"
71
+ print(f"{key:<30} {status:<10}")
72
+ else:
73
+ for key, _ in sorted(variables):
74
+ print(key)
75
+
76
+ except FileNotFoundError as e:
77
+ print(f"Error: {e}", file=sys.stderr)
78
+ sys.exit(1)
79
+ except Exception as e:
80
+ print(f"Error reading file: {e}", file=sys.stderr)
81
+ sys.exit(1)
82
+
83
+
84
+ def check_key(filepath: Path, key_name: str) -> None:
85
+ """Check if a specific key exists in the .env file."""
86
+ try:
87
+ variables = parse_env_file(filepath)
88
+
89
+ for key, has_value in variables:
90
+ if key == key_name:
91
+ if has_value:
92
+ print(f"✓ {key_name} is defined with a value")
93
+ else:
94
+ print(f"⚠ {key_name} is defined but empty")
95
+ sys.exit(0)
96
+
97
+ print(f"✗ {key_name} is not defined")
98
+ sys.exit(1)
99
+
100
+ except FileNotFoundError as e:
101
+ print(f"Error: {e}", file=sys.stderr)
102
+ sys.exit(1)
103
+ except Exception as e:
104
+ print(f"Error reading file: {e}", file=sys.stderr)
105
+ sys.exit(1)
106
+
107
+
108
+ def count_variables(filepath: Path) -> None:
109
+ """Count the number of variables defined."""
110
+ try:
111
+ variables = parse_env_file(filepath)
112
+ total = len(variables)
113
+ with_values = sum(1 for _, has_value in variables if has_value)
114
+ empty = total - with_values
115
+
116
+ print(f"Total variables: {total}")
117
+ if total > 0:
118
+ print(f" With values: {with_values}")
119
+ print(f" Empty: {empty}")
120
+
121
+ except FileNotFoundError as e:
122
+ print(f"Error: {e}", file=sys.stderr)
123
+ sys.exit(1)
124
+ except Exception as e:
125
+ print(f"Error reading file: {e}", file=sys.stderr)
126
+ sys.exit(1)
127
+
128
+
129
+ def validate_syntax(filepath: Path) -> None:
130
+ """Validate the syntax of the .env file."""
131
+ try:
132
+ if not filepath.exists():
133
+ print(f"Error: File not found: {filepath}", file=sys.stderr)
134
+ sys.exit(1)
135
+
136
+ issues = []
137
+ valid_lines = 0
138
+
139
+ with open(filepath, 'r', encoding='utf-8') as f:
140
+ for line_num, line in enumerate(f, 1):
141
+ original_line = line
142
+ line = line.strip()
143
+
144
+ # Skip empty lines and comments
145
+ if not line or line.startswith('#'):
146
+ continue
147
+
148
+ # Check for valid KEY=value pattern
149
+ if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*\s*=.*$', line):
150
+ if '=' in line:
151
+ issues.append(f"Line {line_num}: Invalid key format")
152
+ else:
153
+ issues.append(f"Line {line_num}: Missing '=' separator")
154
+ else:
155
+ valid_lines += 1
156
+
157
+ if issues:
158
+ print(f"✗ Found {len(issues)} syntax issue(s):")
159
+ for issue in issues[:10]: # Show first 10 issues
160
+ print(f" {issue}")
161
+ if len(issues) > 10:
162
+ print(f" ... and {len(issues) - 10} more")
163
+ sys.exit(1)
164
+ else:
165
+ print(f"✓ Syntax valid ({valid_lines} variables defined)")
166
+
167
+ except Exception as e:
168
+ print(f"Error reading file: {e}", file=sys.stderr)
169
+ sys.exit(1)
170
+
171
+
172
+ def main():
173
+ parser = argparse.ArgumentParser(
174
+ description="Safely inspect .env files without exposing sensitive values",
175
+ formatter_class=argparse.RawDescriptionHelpFormatter,
176
+ epilog="""
177
+ Examples:
178
+ env-safe list # List all keys
179
+ env-safe list --status # List keys with defined/empty status
180
+ env-safe check API_KEY # Check if API_KEY exists
181
+ env-safe count # Count variables
182
+ env-safe validate # Check syntax
183
+ env-safe list --file config.env # Use different file
184
+
185
+ This tool is designed to be safe for automated tools like Claude Code,
186
+ preventing accidental exposure of sensitive environment values.
187
+ """
188
+ )
189
+
190
+ parser.add_argument(
191
+ '--file', '-f',
192
+ default='.env',
193
+ help='Path to env file (default: .env)'
194
+ )
195
+
196
+ subparsers = parser.add_subparsers(dest='command', help='Commands')
197
+
198
+ # List command
199
+ list_parser = subparsers.add_parser('list', help='List all environment keys')
200
+ list_parser.add_argument(
201
+ '--status', '-s',
202
+ action='store_true',
203
+ help='Show whether each key has a value'
204
+ )
205
+
206
+ # Check command
207
+ check_parser = subparsers.add_parser('check', help='Check if a key exists')
208
+ check_parser.add_argument('key', help='The key name to check')
209
+
210
+ # Count command
211
+ count_parser = subparsers.add_parser('count', help='Count variables')
212
+
213
+ # Validate command
214
+ validate_parser = subparsers.add_parser('validate', help='Validate syntax')
215
+
216
+ args = parser.parse_args()
217
+
218
+ if not args.command:
219
+ parser.print_help()
220
+ sys.exit(1)
221
+
222
+ filepath = Path(args.file)
223
+
224
+ if args.command == 'list':
225
+ list_keys(filepath, args.status)
226
+ elif args.command == 'check':
227
+ check_key(filepath, args.key)
228
+ elif args.command == 'count':
229
+ count_variables(filepath)
230
+ elif args.command == 'validate':
231
+ validate_syntax(filepath)
232
+
233
+
234
+ if __name__ == '__main__':
235
+ main()
@@ -121,39 +121,51 @@ def extract_project_name(original_path: str) -> str:
121
121
  return parts[-1] if parts else "unknown"
122
122
 
123
123
 
124
- def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool, int]:
124
+ def search_keywords_in_file(filepath: Path, keywords: List[str]) -> tuple[bool, int, Optional[str]]:
125
125
  """
126
- Check if all keywords are present in the JSONL file and count lines.
126
+ Check if all keywords are present in the JSONL file, count lines, and extract git branch.
127
127
 
128
128
  Args:
129
129
  filepath: Path to the JSONL file
130
130
  keywords: List of keywords to search for (case-insensitive)
131
131
 
132
132
  Returns:
133
- Tuple of (matches: bool, line_count: int)
133
+ Tuple of (matches: bool, line_count: int, git_branch: Optional[str])
134
134
  - matches: True if ALL keywords are found in the file
135
135
  - line_count: Total number of lines in the file
136
+ - git_branch: Git branch name from the first message that has it, or None
136
137
  """
137
138
  # Convert keywords to lowercase for case-insensitive search
138
139
  keywords_lower = [k.lower() for k in keywords]
139
140
  found_keywords = set()
140
141
  line_count = 0
142
+ git_branch = None
141
143
 
142
144
  try:
143
145
  with open(filepath, 'r', encoding='utf-8') as f:
144
146
  for line in f:
145
147
  line_count += 1
146
148
  line_lower = line.lower()
149
+
150
+ # Extract git branch from JSON if not already found
151
+ if git_branch is None:
152
+ try:
153
+ data = json.loads(line.strip())
154
+ if 'gitBranch' in data and data['gitBranch']:
155
+ git_branch = data['gitBranch']
156
+ except (json.JSONDecodeError, KeyError):
157
+ pass
158
+
147
159
  # Check which keywords are in this line
148
160
  for keyword in keywords_lower:
149
161
  if keyword in line_lower:
150
162
  found_keywords.add(keyword)
151
163
  except Exception:
152
164
  # Skip files that can't be read
153
- return False, 0
165
+ return False, 0, None
154
166
 
155
167
  matches = len(found_keywords) == len(keywords_lower)
156
- return matches, line_count
168
+ return matches, line_count, git_branch
157
169
 
158
170
 
159
171
  def get_session_preview(filepath: Path) -> str:
@@ -187,7 +199,7 @@ def get_session_preview(filepath: Path) -> str:
187
199
  return "No preview available"
188
200
 
189
201
 
190
- def find_sessions(keywords: List[str], global_search: bool = False, claude_home: Optional[str] = None) -> List[Tuple[str, float, int, str, str, str]]:
202
+ def find_sessions(keywords: List[str], global_search: bool = False, claude_home: Optional[str] = None) -> List[Tuple[str, float, int, str, str, str, Optional[str]]]:
191
203
  """
192
204
  Find all Claude Code sessions containing the specified keywords.
193
205
 
@@ -197,7 +209,7 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
197
209
  claude_home: Optional custom Claude home directory (defaults to ~/.claude)
198
210
 
199
211
  Returns:
200
- List of tuples (session_id, modification_time, line_count, project_name, preview, project_path) sorted by modification time
212
+ List of tuples (session_id, modification_time, line_count, project_name, preview, project_path, git_branch) sorted by modification time
201
213
  """
202
214
  matching_sessions = []
203
215
 
@@ -220,12 +232,12 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
220
232
 
221
233
  # Search all JSONL files in this project directory
222
234
  for jsonl_file in project_dir.glob("*.jsonl"):
223
- matches, line_count = search_keywords_in_file(jsonl_file, keywords)
235
+ matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
224
236
  if matches:
225
237
  session_id = jsonl_file.stem
226
238
  mod_time = jsonl_file.stat().st_mtime
227
239
  preview = get_session_preview(jsonl_file)
228
- matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path))
240
+ matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path, git_branch))
229
241
 
230
242
  progress.advance(task)
231
243
  else:
@@ -234,12 +246,12 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
234
246
  project_name = extract_project_name(original_path)
235
247
 
236
248
  for jsonl_file in project_dir.glob("*.jsonl"):
237
- matches, line_count = search_keywords_in_file(jsonl_file, keywords)
249
+ matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
238
250
  if matches:
239
251
  session_id = jsonl_file.stem
240
252
  mod_time = jsonl_file.stat().st_mtime
241
253
  preview = get_session_preview(jsonl_file)
242
- matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path))
254
+ matching_sessions.append((session_id, mod_time, line_count, project_name, preview, original_path, git_branch))
243
255
  else:
244
256
  # Search current project only
245
257
  claude_dir = get_claude_project_dir(claude_home)
@@ -251,12 +263,12 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
251
263
 
252
264
  # Search all JSONL files in the directory
253
265
  for jsonl_file in claude_dir.glob("*.jsonl"):
254
- matches, line_count = search_keywords_in_file(jsonl_file, keywords)
266
+ matches, line_count, git_branch = search_keywords_in_file(jsonl_file, keywords)
255
267
  if matches:
256
268
  session_id = jsonl_file.stem
257
269
  mod_time = jsonl_file.stat().st_mtime
258
270
  preview = get_session_preview(jsonl_file)
259
- matching_sessions.append((session_id, mod_time, line_count, project_name, preview, os.getcwd()))
271
+ matching_sessions.append((session_id, mod_time, line_count, project_name, preview, os.getcwd(), git_branch))
260
272
 
261
273
  # Sort by modification time (newest first)
262
274
  matching_sessions.sort(key=lambda x: x[1], reverse=True)
@@ -264,7 +276,7 @@ def find_sessions(keywords: List[str], global_search: bool = False, claude_home:
264
276
  return matching_sessions
265
277
 
266
278
 
267
- def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]], keywords: List[str], stderr_mode: bool = False, num_matches: int = 10) -> Optional[Tuple[str, str]]:
279
+ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str, Optional[str]]], keywords: List[str], stderr_mode: bool = False, num_matches: int = 10) -> Optional[Tuple[str, str]]:
268
280
  """Display interactive UI for session selection."""
269
281
  if not RICH_AVAILABLE:
270
282
  return None
@@ -292,16 +304,19 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]]
292
304
  table.add_column("#", style="bold yellow", width=3)
293
305
  table.add_column("Session ID", style="dim")
294
306
  table.add_column("Project", style="green")
307
+ table.add_column("Branch", style="magenta")
295
308
  table.add_column("Date", style="blue")
296
309
  table.add_column("Lines", style="cyan", justify="right")
297
310
  table.add_column("Preview", style="white", overflow="fold")
298
311
 
299
- for idx, (session_id, mod_time, line_count, project_name, preview, _) in enumerate(display_sessions, 1):
312
+ for idx, (session_id, mod_time, line_count, project_name, preview, _, git_branch) in enumerate(display_sessions, 1):
300
313
  mod_date = datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M')
314
+ branch_display = git_branch if git_branch else "N/A"
301
315
  table.add_row(
302
316
  str(idx),
303
317
  session_id[:8] + "...",
304
318
  project_name,
319
+ branch_display,
305
320
  mod_date,
306
321
  str(line_count),
307
322
  preview
@@ -327,6 +342,11 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]]
327
342
  console=ui_console
328
343
  )
329
344
 
345
+ # Handle empty input
346
+ if not choice or not choice.strip():
347
+ ui_console.print("[red]Invalid choice. Please try again.[/red]")
348
+ continue
349
+
330
350
  # Restore stdout
331
351
  if stderr_mode:
332
352
  sys.stdout.close()
@@ -340,7 +360,10 @@ def display_interactive_ui(sessions: List[Tuple[str, float, int, str, str, str]]
340
360
  except KeyboardInterrupt:
341
361
  ui_console.print("\n[yellow]Cancelled[/yellow]")
342
362
  return None
343
- except (ValueError, EOFError):
363
+ except EOFError:
364
+ ui_console.print("\n[yellow]Cancelled (EOF)[/yellow]")
365
+ return None
366
+ except ValueError:
344
367
  ui_console.print("[red]Invalid choice. Please try again.[/red]")
345
368
 
346
369
 
@@ -496,12 +519,13 @@ To persist directory changes when resuming sessions:
496
519
  # Fallback: print session IDs as before
497
520
  if not args.shell:
498
521
  print("\nMatching sessions:")
499
- for idx, (session_id, mod_time, line_count, project_name, preview, project_path) in enumerate(matching_sessions[:args.num_matches], 1):
522
+ for idx, (session_id, mod_time, line_count, project_name, preview, project_path, git_branch) in enumerate(matching_sessions[:args.num_matches], 1):
500
523
  mod_date = datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M:%S')
524
+ branch_display = git_branch if git_branch else "N/A"
501
525
  if getattr(args, 'global'):
502
- print(f"{idx}. {session_id} | {project_name} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
526
+ print(f"{idx}. {session_id} | {project_name} | {branch_display} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
503
527
  else:
504
- print(f"{idx}. {session_id} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
528
+ print(f"{idx}. {session_id} | {branch_display} | {mod_date} | {line_count} lines", file=sys.stderr if args.shell else sys.stdout)
505
529
 
506
530
  if len(matching_sessions) > args.num_matches:
507
531
  print(f"\n... and {len(matching_sessions) - args.num_matches} more sessions", file=sys.stderr if args.shell else sys.stdout)
@@ -510,7 +534,7 @@ To persist directory changes when resuming sessions:
510
534
  if len(matching_sessions) == 1:
511
535
  if not args.shell:
512
536
  print("\nOnly one match found. Resuming automatically...")
513
- session_id, _, _, _, _, project_path = matching_sessions[0]
537
+ session_id, _, _, _, _, project_path, _ = matching_sessions[0]
514
538
  resume_session(session_id, project_path, shell_mode=args.shell)
515
539
  else:
516
540
  try:
@@ -521,10 +545,15 @@ To persist directory changes when resuming sessions:
521
545
  choice = sys.stdin.readline().strip()
522
546
  else:
523
547
  choice = input("\nEnter number to resume session (or Ctrl+C to cancel): ")
548
+
549
+ # Handle empty input or EOF
550
+ if not choice:
551
+ print("Cancelled (EOF)", file=sys.stderr)
552
+ sys.exit(0)
524
553
 
525
554
  idx = int(choice) - 1
526
555
  if 0 <= idx < min(args.num_matches, len(matching_sessions)):
527
- session_id, _, _, _, _, project_path = matching_sessions[idx]
556
+ session_id, _, _, _, _, project_path, _ = matching_sessions[idx]
528
557
  resume_session(session_id, project_path, shell_mode=args.shell)
529
558
  else:
530
559
  print("Invalid choice", file=sys.stderr)
@@ -128,6 +128,19 @@ class TmuxCLIController:
128
128
  output, code = self._run_tmux_command(['display-message', '-p', '#{pane_id}'])
129
129
  return output if code == 0 else None
130
130
 
131
+ def get_current_window_id(self) -> Optional[str]:
132
+ """Get the ID of the current tmux window."""
133
+ # Use TMUX_PANE environment variable to get the pane we're running in
134
+ import os
135
+ current_pane = os.environ.get('TMUX_PANE')
136
+ if current_pane:
137
+ # Get the window ID for this specific pane
138
+ output, code = self._run_tmux_command(['display-message', '-t', current_pane, '-p', '#{window_id}'])
139
+ return output if code == 0 else None
140
+ # Fallback to current window
141
+ output, code = self._run_tmux_command(['display-message', '-p', '#{window_id}'])
142
+ return output if code == 0 else None
143
+
131
144
  def list_panes(self) -> List[Dict[str, str]]:
132
145
  """
133
146
  List all panes in the current window.
@@ -175,8 +188,15 @@ class TmuxCLIController:
175
188
  Returns:
176
189
  Pane ID of the created pane
177
190
  """
191
+ # Get the current window ID to ensure pane is created in this window
192
+ current_window_id = self.get_current_window_id()
193
+
178
194
  cmd = ['split-window']
179
195
 
196
+ # Target the specific window where tmux-cli was called from
197
+ if current_window_id:
198
+ cmd.extend(['-t', current_window_id])
199
+
180
200
  if vertical:
181
201
  cmd.append('-h')
182
202
  else:
@@ -0,0 +1,307 @@
1
+ # Zsh Shell Setup Guide
2
+
3
+ This guide covers the essential setup for a productive Zsh environment with
4
+ Oh My Zsh, plugins, and Starship prompt.
5
+
6
+ ## Prerequisites
7
+
8
+ 1. **Install Homebrew** (macOS package manager)
9
+ ```bash
10
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
11
+ ```
12
+
13
+ 2. **Install Zsh** (usually pre-installed on macOS)
14
+ ```bash
15
+ brew install zsh
16
+ ```
17
+
18
+ ## Oh My Zsh Installation
19
+
20
+ Install Oh My Zsh framework for managing Zsh configuration:
21
+
22
+ ```bash
23
+ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
24
+ ```
25
+
26
+ This creates `~/.oh-my-zsh` directory and a basic `~/.zshrc` file.
27
+
28
+ ## Essential Plugins
29
+
30
+ ### 1. Zsh Autosuggestions
31
+
32
+ Suggests commands as you type based on history and completions:
33
+
34
+ ```bash
35
+ git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions
36
+ ```
37
+
38
+ ### 2. Zsh Syntax Highlighting
39
+
40
+ Provides syntax highlighting for the shell:
41
+
42
+ ```bash
43
+ git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
44
+ ```
45
+
46
+ ### 3. Configure Plugins in .zshrc
47
+
48
+ Add to your `~/.zshrc`:
49
+
50
+ ```bash
51
+ # Oh My Zsh path
52
+ export ZSH="$HOME/.oh-my-zsh"
53
+
54
+ # Plugins configuration
55
+ plugins=(
56
+ git
57
+ zsh-autosuggestions
58
+ zsh-syntax-highlighting
59
+ )
60
+
61
+ # Source Oh My Zsh (commented out if using Starship)
62
+ # source $ZSH/oh-my-zsh.sh
63
+
64
+ # Source autosuggestions manually if needed
65
+ source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
66
+ ```
67
+
68
+ ## Starship Prompt
69
+
70
+ Starship is a minimal, blazing-fast, and customizable prompt for any shell.
71
+
72
+ ### Installation
73
+
74
+ ```bash
75
+ curl -sS https://starship.rs/install.sh | sh
76
+ ```
77
+
78
+ ### Configuration
79
+
80
+ Add to the end of your `~/.zshrc`:
81
+
82
+ ```bash
83
+ eval "$(starship init zsh)"
84
+ ```
85
+
86
+ ### Starship Configuration File
87
+
88
+ You can customize starship by creating/editing `~/.config/starship.toml`.
89
+
90
+ I recommend this `catppuccin` preset:
91
+
92
+ ```bash
93
+ starship preset catppuccin-powerline -o ~/.config/starship.toml
94
+ ```
95
+
96
+ But if you want to tweak it, see the starship config docs.
97
+ https://starship.rs/guide/
98
+
99
+ ## Enhanced Directory Listings with eza
100
+
101
+ Eza is a modern replacement for `ls` with colors, icons, and Git integration.
102
+
103
+ ### Installation
104
+
105
+ ```bash
106
+ brew install eza
107
+ ```
108
+
109
+ ### Configuration
110
+
111
+ Add these aliases to your `~/.zshrc` to replace default `ls` commands:
112
+
113
+ ```bash
114
+ # Replace ls with eza for beautiful directory listings
115
+ alias ls='eza --icons --group-directories-first'
116
+ alias ll='eza -l --icons --group-directories-first --header'
117
+ alias la='eza -la --icons --group-directories-first --header'
118
+ alias lt='eza --tree --icons --level=2'
119
+ alias ltd='eza --tree --icons --level=2 --only-dirs'
120
+
121
+ # Extended eza aliases
122
+ alias l='eza -lbF --git --icons' # list with git status
123
+ alias llm='eza -lbGd --git --sort=modified' # long list, modified date sort
124
+ alias lls='eza -lbhHigmuSa --time-style=long-iso --git --color-scale' # full details
125
+ ```
126
+
127
+ ### Features
128
+
129
+ - **Icons**: Shows file type icons (requires a Nerd Font)
130
+ - **Colors**: Color-codes files by type and permissions
131
+ - **Git Integration**: Shows git status in listings
132
+ - **Tree View**: Built-in tree view with `--tree` flag
133
+ - **Sorting**: Groups directories first by default
134
+
135
+ ### Usage Examples
136
+
137
+ ```bash
138
+ ls # Basic listing with icons
139
+ ll # Long format with details
140
+ la # Show hidden files
141
+ lt # Tree view (2 levels)
142
+ l # List with git status indicators
143
+ ```
144
+
145
+ ## Terminal Title Management
146
+
147
+ Add custom terminal title that shows current directory:
148
+
149
+ ```bash
150
+ # Disable auto-setting terminal title
151
+ DISABLE_AUTO_TITLE="true"
152
+
153
+ # Set terminal title to current directory
154
+ function precmd () {
155
+ echo -ne "\033]0;$(print -rD $PWD)\007"
156
+ }
157
+ precmd
158
+
159
+ # Show command being executed in title with rocket emojis
160
+ function preexec () {
161
+ print -Pn "\e]0;🚀 $(print -rD $PWD) $1 🚀\a"
162
+ }
163
+ ```
164
+
165
+ ## History Search Enhancement
166
+
167
+ ### Option 1: Atuin (Modern Shell History)
168
+
169
+ Install Atuin for enhanced history search:
170
+
171
+ ```bash
172
+ brew install atuin
173
+ ```
174
+
175
+ Configure in `~/.zshrc`:
176
+
177
+ ```bash
178
+ export ATUIN_NOBIND="true"
179
+ eval "$(atuin init zsh)"
180
+ # Bind to up arrow keys (depends on terminal)
181
+ bindkey '^[[A' _atuin_search_widget
182
+ bindkey '^[OA' _atuin_search_widget
183
+ ```
184
+
185
+ ### Option 2: HSTR (Simple History Search)
186
+
187
+ ```bash
188
+ brew install hstr
189
+ ```
190
+
191
+ Configure in `~/.zshrc`:
192
+
193
+ ```bash
194
+ alias hh=hstr
195
+ setopt histignorespace # skip cmds w/ leading space from history
196
+ export HSTR_CONFIG=hicolor # get more colors
197
+ bindkey -s "\C-r" "\C-a hstr -- \C-j" # bind hstr to Ctrl-r
198
+ ```
199
+
200
+ ## Useful Aliases
201
+
202
+ Add these productivity aliases to your `~/.zshrc`:
203
+
204
+ ```bash
205
+ # Git aliases
206
+ alias gsuno="git status -uno" # git status without untracked files
207
+ alias gspu="git stash push -m" # stash with message
208
+ alias gspop="git stash pop" # pop stash
209
+ alias gsl="git stash list" # list stashes
210
+
211
+ # Quick git commit amend and push
212
+ function gcpq() {
213
+ ga -u
214
+ git commit --amend --no-edit
215
+ git push origin main --force-with-lease
216
+ }
217
+
218
+ # Tmux aliases
219
+ alias tmnew="tmux new -s "
220
+ alias tmls="tmux ls"
221
+ alias tma="tmux a -t "
222
+ alias tmk="tmux kill-session -t "
223
+ alias tmd="tmux detach"
224
+ ```
225
+
226
+ ## Completion System
227
+
228
+ Enable advanced tab completion:
229
+
230
+ ```bash
231
+ autoload -Uz compinit
232
+ zstyle ':completion:*' menu select
233
+ fpath+=~/.zfunc
234
+ ```
235
+
236
+ ## Environment Variables
237
+
238
+ Set common environment variables:
239
+
240
+ ```bash
241
+ export EDITOR="nano" # or "vim", "code", etc.
242
+ export TERM=xterm-256color
243
+ ```
244
+
245
+ ## Final .zshrc Structure
246
+
247
+ Your `~/.zshrc` should follow this general structure:
248
+
249
+ ```bash
250
+ # 1. Oh My Zsh configuration
251
+ export ZSH="$HOME/.oh-my-zsh"
252
+ plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
253
+
254
+ # 2. Source additional files
255
+ source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
256
+
257
+ # 3. Environment variables
258
+ export EDITOR="nano"
259
+ export TERM=xterm-256color
260
+
261
+ # 4. Aliases and functions
262
+ alias gsuno="git status -uno"
263
+ # ... more aliases
264
+
265
+ # 5. Terminal title customization
266
+ DISABLE_AUTO_TITLE="true"
267
+ function precmd () { ... }
268
+ function preexec () { ... }
269
+
270
+ # 6. History search tool (Atuin or HSTR)
271
+ eval "$(atuin init zsh)"
272
+
273
+ # 7. Completion system
274
+ autoload -Uz compinit
275
+ zstyle ':completion:*' menu select
276
+
277
+ # 8. Starship prompt (at the very end)
278
+ eval "$(starship init zsh)"
279
+ ```
280
+
281
+ ## Verification
282
+
283
+ After setup, verify everything works:
284
+
285
+ ```bash
286
+ # Reload shell configuration
287
+ source ~/.zshrc
288
+
289
+ # Test autosuggestions (type a partial command and see suggestions)
290
+ # Test syntax highlighting (commands should be colored)
291
+ # Test Starship prompt (should see a styled prompt)
292
+ ```
293
+
294
+ ## Troubleshooting
295
+
296
+ 1. **Slow shell startup**: Comment out unused plugins and features
297
+ 2. **Autosuggestions not working**: Ensure the plugin is properly cloned and
298
+ sourced
299
+ 3. **Starship not showing**: Make sure it's the last line in `.zshrc`
300
+ 4. **Terminal title not updating**: Check if `DISABLE_AUTO_TITLE="true"` is set
301
+
302
+ ## Additional Resources
303
+
304
+ - [Oh My Zsh Documentation](https://github.com/ohmyzsh/ohmyzsh/wiki)
305
+ - [Starship Documentation](https://starship.rs/config/)
306
+ - [Zsh Autosuggestions](https://github.com/zsh-users/zsh-autosuggestions)
307
+ - [Atuin Documentation](https://github.com/atuinsh/atuin)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "0.1.13"
3
+ version = "0.1.17"
4
4
  description = "Collection of tools for working with Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -17,6 +17,7 @@ dev = ["commitizen>=3.0.0"]
17
17
  find-claude-session = "claude_code_tools.find_claude_session:main"
18
18
  vault = "claude_code_tools.dotenv_vault:main"
19
19
  tmux-cli = "claude_code_tools.tmux_cli_controller:main"
20
+ env-safe = "claude_code_tools.env_safe:main"
20
21
 
21
22
  [build-system]
22
23
  requires = ["hatchling"]
@@ -40,7 +41,7 @@ exclude = [
40
41
 
41
42
  [tool.commitizen]
42
43
  name = "cz_conventional_commits"
43
- version = "0.1.13"
44
+ version = "0.1.17"
44
45
  tag_format = "v$version"
45
46
  version_files = [
46
47
  "pyproject.toml:version",