Skip to content

Fix migrated hook path rewriting#20144

Merged
alexsong-oai merged 3 commits intomainfrom
alexs/hooks-migration
Apr 29, 2026
Merged

Fix migrated hook path rewriting#20144
alexsong-oai merged 3 commits intomainfrom
alexs/hooks-migration

Conversation

@alexsong-oai
Copy link
Copy Markdown
Collaborator

@alexsong-oai alexsong-oai commented Apr 29, 2026

Summary

  • Rewrite migrated external-agent hook commands by replacing the full hook script path token instead of only the .claude/hooks/ segment.
  • Preserve quoting around the full rewritten target path so script names with spaces, absolute paths, and shell operators/redirection continue to work.
  • Apply .claude/settings.local.json over .claude/settings.json for config, MCP, and plugin migration so local scope matches Claude settings precedence.
  • Skip legacy command markdown without description frontmatter, including README-style docs under .claude/commands.

Root Cause

The previous hook rewrite handled .claude/hooks/ as a substring replacement. For absolute source commands, that left the original project-root prefix before the newly quoted .codex/hooks directory, producing invalid commands like project/'project/.codex/hooks'/script.sh.

The migration also only used project settings.json for config/MCP/plugin decisions, so local settings such as disabledMcpjsonServers could be ignored even though Claude gives local settings higher precedence than project settings.

Validation

  • just fmt
  • cargo test -p codex-external-agent-migration
  • cargo test -p codex-app-server external_agent_config
  • just fix -p codex-external-agent-migration
  • just fix -p codex-app-server
  • git diff --check

@alexsong-oai alexsong-oai marked this pull request as ready for review April 29, 2026 05:31
@alexsong-oai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2fa2725ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/external-agent-migration/src/lib.rs Outdated
Comment thread codex-rs/external-agent-migration/src/lib.rs Outdated
Comment thread codex-rs/external-agent-migration/src/lib.rs
Comment thread codex-rs/external-agent-migration/src/lib.rs Outdated
@alexsong-oai alexsong-oai force-pushed the alexs/hooks-migration branch from a2fa272 to fd2336e Compare April 29, 2026 05:51
@alexsong-oai alexsong-oai marked this pull request as draft April 29, 2026 05:52
@alexsong-oai alexsong-oai force-pushed the alexs/hooks-migration branch from fd2336e to dd6f8cd Compare April 29, 2026 06:06
@alexsong-oai alexsong-oai marked this pull request as ready for review April 29, 2026 06:18
@alexsong-oai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0491fc67b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +811 to +813
fn is_shell_path_boundary(ch: char) -> bool {
ch.is_whitespace() || matches!(ch, ';' | '|' | '&' | '<' | '>' | '(' | ')')
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include '=' in shell path boundary detection

replace_unquoted_hook_paths relies on shell_path_start, but is_shell_path_boundary does not treat = as a boundary. For commands like HOOK=${CLAUDE_PROJECT_DIR}/.claude/hooks/check.py python3 "$HOOK", rewriting starts at byte 0 and replaces the whole assignment token with a quoted path, dropping HOOK= and breaking execution.

Useful? React with 👍 / 👎.

return Ok(effective);
};
let local_settings = settings_dir.join("settings.local.json");
let Some(local_settings) = read_external_settings(&local_settings)? else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tolerate invalid settings.local.json during merge

effective_external_settings now propagates parse errors from settings.local.json. A malformed local file will abort config/MCP/plugin detection and import even when settings.json is valid. This introduces a migration failure mode that did not exist before local-file merging was added.

Useful? React with 👍 / 👎.

Comment on lines +843 to +845
command.contains(&source_hooks_backslash_path)
|| command.contains(&format!("%{project_dir_env_var}%"))
|| command.contains(&format!("$env:{project_dir_env_var}"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow Windows hook-command detection before early return

looks_like_windows_hook_command returns true if %{PROJECT_DIR}% or $env:{PROJECT_DIR} appears anywhere, and rewrite_hook_command then skips all rewriting. Mixed POSIX commands can keep stale .claude/hooks paths (e.g., python3 /repo/.claude/hooks/check.py && echo %CLAUDE_PROJECT_DIR%) and break after migration.

Useful? React with 👍 / 👎.

Comment on lines +830 to +835
fn is_static_hook_path_suffix(suffix: &str) -> bool {
!suffix.is_empty()
&& !suffix
.chars()
.any(|ch| matches!(ch, '\\' | '$' | '`' | '*' | '?' | '['))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject brace expansions in static hook suffix check

is_static_hook_path_suffix blocks globs/vars but allows { and }. A command like .claude/hooks/{lint,fmt}.sh is treated as static and rewritten to one single-quoted literal path, which disables brace expansion and can make valid hook commands fail after migration.

Useful? React with 👍 / 👎.

@alexsong-oai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea252450d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
let path_start = shell_path_start(&rewritten, source_hooks_start);
let path_end = shell_path_end(&rewritten, source_hooks_start + source_hooks_path.len());
if is_assignment_value_start(&rewritten, path_start) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rewrite hook paths in option values after '='

Avoid skipping every source path that appears immediately after =. The is_assignment_value_start guard was meant for env assignments, but it also blocks valid rewrites in arguments like --script=${CLAUDE_PROJECT_DIR}/.claude/hooks/check.py. Those commands retain stale .claude/hooks paths after migration and can fail once legacy hooks are removed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine

!suffix.is_empty()
&& !suffix
.chars()
.any(|ch| matches!(ch, '\\' | '$' | '`' | '*' | '?' | '[' | '{' | '}'))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rewrite escaped hook filenames instead of skipping them

is_static_hook_path_suffix rejects any \\ in the suffix, so commands like python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/my\ script.py are never rewritten. The migrated command keeps the old .claude/hooks path and breaks once legacy hooks are removed. This is a regression for valid shell-escaped filenames with spaces.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine

@alexsong-oai alexsong-oai merged commit d92c909 into main Apr 29, 2026
35 of 36 checks passed
@alexsong-oai alexsong-oai deleted the alexs/hooks-migration branch April 29, 2026 07:46
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants