Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/tools/amplihack/hooks/session_start.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/session_start.py",
"timeout": 10
}
]
Expand All @@ -48,7 +48,7 @@
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/tools/amplihack/hooks/stop.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/stop.py",
"timeout": 120
}
]
Expand All @@ -60,7 +60,7 @@
"hooks": [
{
"type": "command",
"command": ".claude/tools/amplihack/hooks/post_tool_use.py"
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/post_tool_use.py"
}
]
}
Expand All @@ -70,7 +70,7 @@
"hooks": [
{
"type": "command",
"command": ".claude/tools/amplihack/hooks/pre_compact.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/pre_compact.py",
"timeout": 30
}
]
Expand All @@ -81,7 +81,7 @@
"hooks": [
{
"type": "command",
"command": ".claude/tools/amplihack/hooks/session_end.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/session_end.py",
"timeout": 10
}
]
Expand All @@ -90,7 +90,7 @@
},
"statusLine": {
"type": "command",
"command": ".claude/tools/statusline.sh"
"command": "$HOME/.amplihack/.claude/tools/statusline.sh"
},
"enabledPlugins": {
"pyright@claude-code-lsps": true,
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,4 @@ AUTO_VERSION_CODE_REVIEW.md
# Ephemeral test scripts and build artifacts
test-worktree-isolation.sh
target/
outputs/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ backend-path = ["."]

[project]
name = "amplihack"
version = "0.5.41"
version = "0.5.42"
description = "Amplifier bundle for agentic coding with comprehensive skills, recipes, and workflows"
requires-python = ">=3.11"
dependencies = [
Expand Down
24 changes: 21 additions & 3 deletions src/amplihack/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,25 @@ def validate_hook_paths(hook_system, hooks_to_validate, hooks_dir_path):
def update_hook_paths(settings, hook_system, hooks_to_update, hooks_dir_path):
"""Update hook paths for a given hook system (amplihack or xpia).

This function ensures all hook paths in settings.json are absolute paths,
enabling hooks to work from ANY working directory (cross-codebase functionality).

Path expansion behavior:
- Expands ~ (tilde) to user home directory via os.path.expanduser()
- Expands $VAR and ${VAR} environment variables via os.path.expandvars()
- Converts relative paths to absolute using os.path.join()

This is CRITICAL for cross-directory execution:
- Hooks must work when Claude Code runs from ANY codebase
- Relative paths would break when working directory changes
- Absolute paths guarantee hooks are always found

Args:
settings: Settings dictionary to update
hook_system: Name of the hook system (e.g., "amplihack", "xpia")
hooks_to_update: List of dicts with keys: type, file, timeout (optional), matcher (optional)
hooks_dir_path: Relative path to hooks directory (e.g., ".claude/tools/xpia/hooks")
hooks_dir_path: MUST be absolute path to hooks directory after expansion
(e.g., "/home/user/.amplihack/.claude/tools/amplihack/hooks")

Returns:
Number of hooks updated
Expand All @@ -124,8 +138,12 @@ def update_hook_paths(settings, hook_system, hooks_to_update, hooks_dir_path):
timeout = hook_info.get("timeout")
matcher = hook_info.get("matcher")

# Use forward slashes for relative paths (cross-platform JSON compatibility)
hook_path = f"{hooks_dir_path}/{hook_file}"
# CRITICAL: Path expansion ensures cross-directory execution
# Expand environment variables ($HOME) and user directory (~) to absolute paths
# This ensures hooks work from ANY working directory (cross-codebase functionality)
hook_path = os.path.abspath(
os.path.expanduser(os.path.expandvars(f"{hooks_dir_path}/{hook_file}"))
)

if hook_type not in settings.get("hooks", {}):
# Add missing hook configuration
Expand Down
10 changes: 5 additions & 5 deletions src/amplihack/utils/uvx_settings_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/tools/amplihack/hooks/session_start.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/session_start.py",
"timeout": 10
}
]
Expand All @@ -45,7 +45,7 @@
"hooks": [
{
"type": "command",
"command": "$HOME/.claude/tools/amplihack/hooks/stop.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/stop.py",
"timeout": 120
}
]
Expand All @@ -57,7 +57,7 @@
"hooks": [
{
"type": "command",
"command": ".claude/tools/amplihack/hooks/post_tool_use.py"
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/post_tool_use.py"
}
]
}
Expand All @@ -67,7 +67,7 @@
"hooks": [
{
"type": "command",
"command": ".claude/tools/amplihack/hooks/pre_compact.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/pre_compact.py",
"timeout": 30
}
]
Expand All @@ -78,7 +78,7 @@
"hooks": [
{
"type": "command",
"command": ".claude/tools/amplihack/hooks/session_end.py",
"command": "$HOME/.amplihack/.claude/tools/amplihack/hooks/session_end.py",
"timeout": 10
}
]
Expand Down
108 changes: 108 additions & 0 deletions tests/outside-in/test_hook_cross_directory_execution.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
scenario:
name: "Hook Path Configuration - Cross-Directory Execution"
description: "Verifies hooks work when amplihack runs from different working directories (cross-codebase functionality)"
type: cli
tags: [critical, hooks, cross-directory, integration]

prerequisites:
- "amplihack is installed (via pip or UVX)"
- "~/.amplihack/.claude/settings.json exists"

variables:
test_dir_1: "/tmp/test-amplihack-dir1"
test_dir_2: "${HOME}/test-amplihack-dir2"
test_dir_3: "${HOME}/.amplihack"

steps:
# Verify settings.json has absolute paths first
- action: launch
target: "python3"
args:
- "-c"
- "import json; f=open('${HOME}/.amplihack/.claude/settings.json'); s=json.load(f); hooks=[h['command'] for cfg in s['hooks'].values() for c in cfg for h in c['hooks']]; print('\\n'.join(hooks)); assert all(h.startswith('/') for h in hooks), 'All paths must be absolute'"
description: "Verify all hook paths are absolute before testing"
timeout: 10s

- action: verify_output
contains: "/.amplihack/.claude/tools/amplihack/hooks"
description: "Hook paths should contain absolute path"

- action: verify_exit_code
expected: 0
description: "Validation should pass"

# Test 1: Run from /tmp directory
- action: launch
target: "bash"
args:
- "-c"
- 'cd ${test_dir_1} 2>/dev/null || mkdir -p ${test_dir_1}; cd ${test_dir_1}; pwd; python3 -c ''import sys; sys.path.insert(0, "${PWD}/src"); from amplihack.settings import ensure_settings_json; print("CWD:", __import__("os").getcwd()); result = ensure_settings_json(); print("Result:", result)'''
description: "Test hook setup from /tmp directory"
timeout: 30s

- action: verify_output
contains: "${test_dir_1}"
description: "Should run from test directory 1"

- action: verify_output
contains: "Result: True"
description: "Settings configuration should succeed"

# Test 2: Run from home directory
- action: launch
target: "bash"
args:
- "-c"
- 'cd ${test_dir_2} 2>/dev/null || mkdir -p ${test_dir_2}; cd ${test_dir_2}; pwd; python3 -c ''import sys; sys.path.insert(0, "${PWD}/src"); from amplihack.settings import ensure_settings_json; print("CWD:", __import__("os").getcwd()); result = ensure_settings_json(); print("Result:", result)'''
description: "Test hook setup from home subdirectory"
timeout: 30s

- action: verify_output
contains: "${test_dir_2}"
description: "Should run from test directory 2"

- action: verify_output
contains: "Result: True"
description: "Settings configuration should succeed"

# Test 3: Run from ~/.amplihack directory
- action: launch
target: "bash"
args:
- "-c"
- 'cd ${test_dir_3}; pwd; python3 -c ''import sys; sys.path.insert(0, "${PWD}/src"); from amplihack.settings import ensure_settings_json; print("CWD:", __import__("os").getcwd()); result = ensure_settings_json(); print("Result:", result)'''
description: "Test hook setup from ~/.amplihack directory"
timeout: 30s

- action: verify_output
contains: "${test_dir_3}"
description: "Should run from test directory 3"

- action: verify_output
contains: "Result: True"
description: "Settings configuration should succeed from ANY directory"

# Final verification: All hook paths are still absolute after cross-directory runs
- action: launch
target: "python3"
args:
- "-c"
- "import json, os; f=open('${HOME}/.amplihack/.claude/settings.json'); s=json.load(f); hooks=[h['command'] for cfg in s['hooks'].values() for c in cfg for h in c['hooks']]; print('Final hook paths:'); [print(f' {h}') for h in hooks]; assert all(os.path.isabs(h) for h in hooks), 'FAIL: Relative paths found'"
description: "Final verification - all paths remain absolute"
timeout: 10s

- action: verify_output
contains: "Final hook paths:"
description: "Should list all hook paths"

- action: verify_exit_code
expected: 0
description: "All paths should be absolute (no assertion error)"

cleanup:
- action: launch
target: "bash"
args:
- "-c"
- "rm -rf ${test_dir_1} ${test_dir_2}"
description: "Clean up test directories"
66 changes: 66 additions & 0 deletions tests/outside-in/test_hook_path_absolute_uvx.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
scenario:
name: "Hook Path Configuration - UVX Fresh Install"
description: "Verifies that fresh UVX installation produces settings.json with absolute hook paths (not relative .claude/... paths)"
type: cli
tags: [smoke, critical, hooks, path-configuration]

prerequisites:
- "uvx command is available"
- "git branch feat/issue-2408-fix-hook-path-relative-paths exists on remote"

environment:
variables:
TEST_BRANCH: "feat/issue-2408-fix-hook-path-relative-paths"
GITHUB_REPO: "https://github.com/rysweet/amplihack"

steps:
# Test 1: Fresh UVX installation
- action: launch
target: "uvx"
args:
- "--from"
- "git+${GITHUB_REPO}@${TEST_BRANCH}"
- "amplihack"
- "--version"
description: "Install amplihack via UVX from feature branch"
timeout: 120s

- action: verify_output
contains: "0.5"
description: "Version output should appear"
timeout: 5s

- action: verify_exit_code
expected: 0
description: "Installation should succeed"

# Test 2: Verify settings.json has absolute paths
- action: launch
target: "cat"
args: ["${HOME}/.amplihack/.claude/settings.json"]
description: "Read generated settings.json"
timeout: 5s

- action: verify_output
contains: "${HOME}/.amplihack/.claude/tools/amplihack/hooks"
description: "Hook paths should be absolute (contains full HOME path)"
timeout: 2s

- action: verify_output
not_contains: '".claude/tools/amplihack/hooks'
description: "Hook paths should NOT be relative (.claude/... prefix)"
timeout: 2s

- action: verify_output
not_contains: "$HOME"
description: "Variables should be expanded (no $HOME left)"
timeout: 2s

- action: capture_output
save_as: "settings-json-content.txt"
description: "Save settings.json for evidence"

cleanup:
- action: send_signal
signal: SIGTERM
description: "Ensure no processes left running"
Loading
Loading