Report the local command working directory in PreToolUse hooks (#57198)

## Why

`PreToolUse` hooks receive the host working directory even when a local
`exec_command` call specifies a different `workdir`. This prevents hooks from
seeing the directory in which the command will execute.

## What changed

- Share working-directory resolution between command execution and hook input,
  using the selected environment and the command's `workdir`.
- Report the resolved local directory in the hook input's `cwd`. Keep hook
  processes in the host directory and retain the host `cwd` for remote commands.
- Reject invalid environment selections or paths before running hooks and record
  failures in tool-result telemetry.

## Testing

Add unit and integration coverage for default, empty, relative, and absolute
working directories; Windows path conventions; remote commands; hook denial and
input rewriting; and invalid arguments that fail without running hooks.

GitOrigin-RevId: 97f71f6d3e21a5d95a7abf5384f06f4993241797
This commit is contained in:
Andrei Eternal
2026-10-26 19:11:28 +00:00
committed by copyberry
parent 60e4d10ad9
commit fe15bd38de
9 changed files with 395 additions and 13 deletions
+9
View File
@@ -646,6 +646,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd.clone(),
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -666,6 +667,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
turn_id: "turn-1".to_string(),
subagent: None,
cwd,
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -740,6 +742,7 @@ async fn requirements_managed_hooks_execute_windows_command_override() {
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd(),
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -1142,6 +1145,7 @@ fn requirements_managed_hooks_load_when_managed_dir_is_missing() {
turn_id: "turn-1".to_string(),
subagent: None,
cwd,
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -1575,6 +1579,7 @@ fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
turn_id: "turn-1".to_string(),
subagent: None,
cwd,
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -1672,6 +1677,7 @@ fn profile_user_layers_load_shared_hooks_json_once() {
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd(),
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -2112,6 +2118,7 @@ print(json.dumps({
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd(),
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -2147,6 +2154,7 @@ print(json.dumps({
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd(),
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),
@@ -2853,6 +2861,7 @@ async fn mcp_tool_hooks_expand_event_input_and_apply_pre_tool_decisions() {
turn_id: "turn-1".to_string(),
subagent: None,
cwd: cwd(),
tool_cwd: None,
transcript_path: None,
model: "gpt-test".to_string(),
permission_mode: "default".to_string(),