fix: rewrite Windows backslash paths in hook commands#609
Open
danielmeppiel wants to merge 2 commits intomainfrom
Open
fix: rewrite Windows backslash paths in hook commands#609danielmeppiel wants to merge 2 commits intomainfrom
danielmeppiel wants to merge 2 commits intomainfrom
Conversation
The relative-path regex in _rewrite_command_for_target only matched Unix-style ./ references. Windows-style .\ paths in the 'windows' hook key were silently skipped -- scripts not copied, paths not rewritten. Extend the regex from r'(\./[^\s]+)' to r'(\.[/\\][^\s]+)' to match both ./ and .\ prefixes. Normalize backslashes to forward slashes for path resolution and file lookup. Fixes #520 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes hook command rewriting so Windows-style relative script paths using backslashes (for example, .\scripts\scan.ps1 and ${CLAUDE_PLUGIN_ROOT}\...) are detected, normalized, and rewritten to the installed scripts location, ensuring referenced scripts are copied and hook JSON is updated correctly.
Changes:
- Expand
${CLAUDE_PLUGIN_ROOT}and relative-path regex matching to accept both/and\, and normalize\to/beforePathresolution. - Add unit tests to cover backslash-path rewriting scenarios (direct relative paths, prefixed commands, plugin root, traversal rejection, and end-to-end hook JSON rewrites).
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/integration/hook_integrator.py |
Extends command rewriting to match and normalize Windows backslash path variants for script discovery + rewriting. |
tests/unit/integration/test_hook_integrator.py |
Adds coverage for backslash rewriting, including end-to-end _rewrite_hooks_data() behavior. |
Copilot's findings
Comments suppressed due to low confidence (3)
tests/unit/integration/test_hook_integrator.py:1403
test_rewrite_hooks_data_windows_backslash_flat()appears to accidentally include the entire body (and docstring string literal) of the existing forward-slash windows-key test starting at line 1399. This makes the mid-function triple-quoted string a no-op (not a docstring) and effectively combines two independent test cases into one, which is easy to miss and hard to maintain. Split this into separatedeftest functions (backslash case vs forward-slash regression) and remove the stray in-function string literal/setup duplication.
assert ".github/hooks/scripts/my-pkg/scripts/validate.sh" in hook["bash"]
assert ".github/hooks/scripts/my-pkg/scripts/validate.ps1" in hook["windows"]
assert "\\" not in hook["windows"]
assert len(scripts) == 2
"""Test _rewrite_hooks_data handles windows key in flat format (GitHub Copilot)."""
pkg_dir = temp_project / "pkg"
(pkg_dir / "scripts").mkdir(parents=True)
(pkg_dir / "scripts" / "validate.sh").write_text("#!/bin/bash")
(pkg_dir / "scripts" / "validate.ps1").write_text("Write-Host 'ok'")
src/apm_cli/integration/hook_integrator.py:248
- The path-traversal guard here is implemented inline via
resolve()+Path.is_relative_to(). Since this code builds filesystem paths from hook JSON content (external/package-controlled input), it should use the centralized path-security helpers (validate_path_segmentson the normalized relative path andensure_path_withinfor containment) to keep behavior consistent and avoid drift across call sites.
This issue also appears on line 259 of the same file.
# Normalize backslashes to forward slashes before Path construction
# (on Unix, Path treats backslashes as literal filename chars)
rel_path = match.group(1).replace('\\', '/').lstrip('/')
source_file = (package_path / rel_path).resolve()
# Reject path traversal outside the package directory
if not source_file.is_relative_to(package_path.resolve()):
continue
src/apm_cli/integration/hook_integrator.py:270
- Same as above for
.//.\rewriting: the code constructssource_file = (resolve_base / rel_path).resolve()and then checksis_relative_to(package_path.resolve()). Prefervalidate_path_segments(rel_path, context=...)(after backslash normalization) andensure_path_within(resolve_base / rel_path, package_path)so traversal is rejected through the shared, documented mechanism.
# Resolve from hook file's directory if available, else fall back to package root
resolve_base = hook_file_dir if hook_file_dir else package_path
rel_pattern = r'(\.[\\/][^\s]+)'
for match in re.finditer(rel_pattern, new_command):
rel_ref = match.group(1)
# Normalize to forward slashes for path resolution
rel_path = rel_ref[2:].replace('\\', '/')
source_file = (resolve_base / rel_path).resolve()
# Reject path traversal outside the package directory
if not source_file.is_relative_to(package_path.resolve()):
continue
- Files reviewed: 2/2 changed files
- Comments generated: 0
Tests sharing a temp_project fixture would fail when multiple tests created the same subdirectory. Add exist_ok=True across all mkdir calls in the test file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #520
Problem
The
windowskey in hook JSON uses backslash paths (.\\secrets-scanner\\scan-secrets.ps1), but_rewrite_command_for_target()only matched Unix-style./references. Result:commandpaths were rewritten correctly butwindowspaths were silently skipped -- scripts not copied, paths not rewritten.Reported by @dstamand-msft with a concrete reproduction showing the deployed hook JSON has the
commandpath rewritten butwindowsstill pointing to the original relative path.Fix
Extend the relative-path regex from
r'(\./[^\s]+)'tor'(\.[/\\\\][^\s]+)'to match both./and.\prefixes. Backslashes are normalized to forward slashes for path resolution and file lookup.Before:
{ "command": ".github/hooks/scripts/pkg/scan.sh", "windows": "pwsh -File .\\\\secrets-scanner\\\\scan.ps1" }After:
{ "command": ".github/hooks/scripts/pkg/scan.sh", "windows": "pwsh -File .github/hooks/scripts/pkg/secrets-scanner/scan.ps1" }Tests
4 new tests covering:
.\\path\\script.ps1)3678 tests pass.