Skip to content

Commit a7b3e5e

Browse files
committed
fix(agent-setup): address review feedback
1 parent 8d5e4d3 commit a7b3e5e

File tree

4 files changed

+96
-20
lines changed

4 files changed

+96
-20
lines changed

.agents/AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ Security/config notes:
156156
- Keep credentials and machine-specific secrets in environment variables or
157157
local untracked files, never in committed agent config.
158158
- The shared Claude settings intentionally deny reading `./.env` and
159-
`./.env.*`. If a task genuinely requires inspecting local env overrides, get
159+
`./.env.*`, and they do not auto-approve Bash commands by default. If a task
160+
genuinely requires inspecting local env overrides or shell access, get
160161
explicit user approval first instead of weakening the default policy.
161162
- For authenticated MCP servers or provider-specific config additions, prefer
162163
secret injection via environment variables rather than committed inline

.agents/config.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,7 @@
1313
"claude": {
1414
"settings": {
1515
"permissions": {
16-
"allow": [
17-
"Bash(find:*)",
18-
"Bash(rg:*)",
19-
"Bash(grep:*)",
20-
"Bash(ls:*)",
21-
"Bash(cat:*)",
22-
"Bash(head:*)",
23-
"Bash(tail:*)"
24-
],
16+
"allow": [],
2517
"deny": [
2618
"Read(./.env)",
2719
"Read(./.env.*)"

scripts/agents/sync-agent-shims.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,22 @@ def remove_path(path: Path) -> None:
158158
path.unlink()
159159

160160

161+
def expected_symlink_target(path: Path, target: Path) -> Path:
162+
return Path(os.path.relpath(target, start=path.parent))
163+
164+
161165
def is_matching_symlink(path: Path, target: Path) -> bool:
162166
if not path.is_symlink():
163167
return False
164-
return path.resolve() == target.resolve()
168+
return Path(os.readlink(path)) == expected_symlink_target(path, target)
169+
170+
171+
def is_within_repo(path: Path, repo_root: Path) -> bool:
172+
try:
173+
path.resolve().relative_to(repo_root.resolve())
174+
return True
175+
except ValueError:
176+
return False
165177

166178

167179
def find_unexpected_children(path: Path, expected_children: set[str]) -> list[Path]:
@@ -238,21 +250,41 @@ def sync_symlink_outputs(
238250
continue
239251

240252
remove_path(path)
241-
relative_target = Path(os.path.relpath(target, start=path.parent))
242-
path.symlink_to(relative_target, target_is_directory=target.is_dir())
253+
path.symlink_to(
254+
expected_symlink_target(path, target), target_is_directory=target.is_dir()
255+
)
243256
print(f"Linked {path}")
244257

245258
return has_mismatch
246259

247260

248261
def sync_managed_directories(
249-
managed_directories: list[dict[str, Any]], check_mode: bool
262+
managed_directories: list[dict[str, Any]], repo_root: Path, check_mode: bool
250263
) -> bool:
251264
has_mismatch = False
252265

253266
for directory in managed_directories:
254267
path: Path = directory["path"]
255268
expected_children: set[str] = directory["expected_children"]
269+
270+
if path.is_symlink():
271+
message = f"Managed generated shim directory must not be a symlink: {path}"
272+
if check_mode:
273+
has_mismatch = True
274+
print_error(message)
275+
continue
276+
raise RuntimeError(message)
277+
278+
if path.exists() and not is_within_repo(path, repo_root):
279+
message = (
280+
f"Managed generated shim directory resolves outside the repository: {path}"
281+
)
282+
if check_mode:
283+
has_mismatch = True
284+
print_error(message)
285+
continue
286+
raise RuntimeError(message)
287+
256288
unexpected_children = find_unexpected_children(path, expected_children)
257289

258290
if not unexpected_children:
@@ -319,7 +351,7 @@ def main() -> int:
319351
skills_root = repo_root / ".agents" / "skills"
320352
shared_skill_names = sorted(
321353
entry.name
322-
for entry in skills_root.iterdir()
354+
for entry in (skills_root.iterdir() if skills_root.exists() else [])
323355
if entry.is_dir() and (entry / "SKILL.md").exists()
324356
)
325357

@@ -354,7 +386,8 @@ def main() -> int:
354386

355387
has_mismatch = sync_symlink_outputs(symlink_outputs, check_mode) or has_mismatch
356388
has_mismatch = (
357-
sync_managed_directories(managed_directories, check_mode) or has_mismatch
389+
sync_managed_directories(managed_directories, repo_root, check_mode)
390+
or has_mismatch
358391
)
359392

360393
return 1 if check_mode and has_mismatch else 0

tests/test_sync_agent_shims.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import os
45
import subprocess
56
import sys
67
from pathlib import Path
@@ -101,11 +102,9 @@ def test_sync_agent_shims_generates_expected_outputs(tmp_path: Path) -> None:
101102
],
102103
}
103104
assert (repo_root / "AGENTS.md").is_symlink()
104-
assert (repo_root / "AGENTS.md").resolve() == (
105-
repo_root / ".agents" / "AGENTS.md"
106-
).resolve()
105+
assert Path(os.readlink(repo_root / "AGENTS.md")) == Path(".agents/AGENTS.md")
107106
assert (repo_root / "CLAUDE.md").is_symlink()
108-
assert (repo_root / "CLAUDE.md").resolve() == (repo_root / "AGENTS.md").resolve()
107+
assert Path(os.readlink(repo_root / "CLAUDE.md")) == Path("AGENTS.md")
109108
assert (repo_root / ".claude" / "skills" / "example").is_symlink()
110109
assert not (repo_root / ".claude" / "skills" / "stale").exists()
111110
assert (repo_root / ".codex" / "config.toml").exists()
@@ -121,3 +120,54 @@ def test_sync_agent_shims_check_mode_detects_drift(tmp_path: Path) -> None:
121120

122121
assert result.returncode == 1
123122
assert "Out of sync" in result.stderr
123+
124+
125+
def test_sync_agent_shims_check_mode_detects_symlink_target_drift(tmp_path: Path) -> None:
126+
repo_root = make_fixture_repo(tmp_path)
127+
run_script(repo_root)
128+
(repo_root / "CLAUDE.md").unlink()
129+
(repo_root / "CLAUDE.md").symlink_to(".agents/AGENTS.md")
130+
131+
result = run_script(repo_root, "--check")
132+
133+
assert result.returncode == 1
134+
assert "Out of sync symlink" in result.stderr
135+
136+
137+
def test_sync_agent_shims_handles_missing_skills_directory(tmp_path: Path) -> None:
138+
repo_root = make_fixture_repo(tmp_path)
139+
readme_path = repo_root / ".agents" / "skills" / "README.md"
140+
example_path = repo_root / ".agents" / "skills" / "example"
141+
(example_path / "SKILL.md").unlink()
142+
readme_path.unlink()
143+
example_path.rmdir()
144+
(repo_root / ".agents" / "skills").rmdir()
145+
146+
result = run_script(repo_root)
147+
148+
assert result.returncode == 0
149+
managed_dir = repo_root / ".claude" / "skills"
150+
assert managed_dir.exists()
151+
assert list(managed_dir.iterdir()) == []
152+
153+
154+
def test_sync_agent_shims_rejects_symlinked_managed_directories(tmp_path: Path) -> None:
155+
repo_root = make_fixture_repo(tmp_path)
156+
run_script(repo_root)
157+
external_dir = tmp_path / "external-claude-skills"
158+
external_dir.mkdir()
159+
(external_dir / "foreign").mkdir()
160+
managed_dir = repo_root / ".claude" / "skills"
161+
for child in managed_dir.iterdir():
162+
child.unlink()
163+
managed_dir.rmdir()
164+
managed_dir.symlink_to(external_dir, target_is_directory=True)
165+
166+
check_result = run_script(repo_root, "--check")
167+
sync_result = run_script(repo_root)
168+
169+
assert check_result.returncode == 1
170+
assert "must not be a symlink" in check_result.stderr
171+
assert sync_result.returncode == 1
172+
assert "must not be a symlink" in sync_result.stderr
173+
assert (external_dir / "foreign").exists()

0 commit comments

Comments
 (0)