forked from RichardAtCT/claude-code-telegram
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmonitor.py
More file actions
172 lines (143 loc) · 4.83 KB
/
Copy pathmonitor.py
File metadata and controls
172 lines (143 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""Bash directory boundary enforcement for Claude tool calls."""
import shlex
from pathlib import Path
from typing import Optional, Set, Tuple
# Subdirectories under ~/.claude/ that Claude Code uses internally.
_CLAUDE_INTERNAL_SUBDIRS: Set[str] = {"plans", "todos", "settings.json"}
# Commands that modify the filesystem or change context and should have paths checked
_FS_MODIFYING_COMMANDS: Set[str] = {
"mkdir",
"touch",
"cp",
"mv",
"rm",
"rmdir",
"ln",
"install",
"tee",
"cd",
}
# Commands that are read-only or don't take filesystem paths
_READ_ONLY_COMMANDS: Set[str] = {
"cat",
"ls",
"head",
"tail",
"less",
"more",
"which",
"whoami",
"pwd",
"echo",
"printf",
"env",
"printenv",
"date",
"wc",
"sort",
"uniq",
"diff",
"file",
"stat",
"du",
"df",
"tree",
"realpath",
"dirname",
"basename",
}
# Actions / expressions that make ``find`` a filesystem-modifying command
_FIND_MUTATING_ACTIONS: Set[str] = {"-delete", "-exec", "-execdir", "-ok", "-okdir"}
# Bash command separators
_COMMAND_SEPARATORS: Set[str] = {"&&", "||", ";", "|", "&"}
def check_bash_directory_boundary(
command: str,
working_directory: Path,
approved_directory: Path,
) -> Tuple[bool, Optional[str]]:
"""Check if a bash command's paths stay within the approved directory."""
try:
tokens = shlex.split(command)
except ValueError:
# If we can't parse the command, let it through —
# the sandbox will catch it at the OS level
return True, None
if not tokens:
return True, None
# Split tokens into individual commands based on separators
command_chains: list[list[str]] = []
current_chain: list[str] = []
for token in tokens:
if token in _COMMAND_SEPARATORS:
if current_chain:
command_chains.append(current_chain)
current_chain = []
else:
current_chain.append(token)
if current_chain:
command_chains.append(current_chain)
resolved_approved = approved_directory.resolve()
# Check each command in the chain
for cmd_tokens in command_chains:
if not cmd_tokens:
continue
base_command = Path(cmd_tokens[0]).name
# Read-only commands are always allowed
if base_command in _READ_ONLY_COMMANDS:
continue
# Determine if this specific command in the chain needs path validation
needs_check = False
if base_command == "find":
needs_check = any(t in _FIND_MUTATING_ACTIONS for t in cmd_tokens[1:])
elif base_command in _FS_MODIFYING_COMMANDS:
needs_check = True
if not needs_check:
continue
# Check each argument for paths outside the boundary
for token in cmd_tokens[1:]:
# Skip flags
if token.startswith("-"):
continue
# Resolve both absolute and relative paths against the working
# directory so that traversal sequences like ``../../evil`` are
# caught instead of being silently allowed.
try:
if token.startswith("/"):
resolved = Path(token).resolve()
else:
resolved = (working_directory / token).resolve()
if not _is_within_directory(resolved, resolved_approved):
return False, (
f"Directory boundary violation: '{base_command}' targets "
f"'{token}' which is outside approved directory "
f"'{resolved_approved}'"
)
except (ValueError, OSError):
# If path resolution fails, the command might be malformed or
# using bash features we can't statically analyze.
# We skip checking this token and rely on the OS-level sandbox.
continue
return True, None
def _is_claude_internal_path(file_path: str) -> bool:
"""Check whether *file_path* points inside ``~/.claude/`` (allowed subdirs only)."""
try:
resolved = Path(file_path).resolve()
home = Path.home().resolve()
claude_dir = home / ".claude"
# Path must be inside ~/.claude/
try:
rel = resolved.relative_to(claude_dir)
except ValueError:
return False
# Must be in one of the known subdirectories (or a known file)
top_part = rel.parts[0] if rel.parts else ""
return top_part in _CLAUDE_INTERNAL_SUBDIRS
except Exception:
return False
def _is_within_directory(path: Path, directory: Path) -> bool:
"""Check if path is within directory."""
try:
path.relative_to(directory)
return True
except ValueError:
return False