Project hooks in linked worktrees appear to be loaded from the primary checkout but executed from the linked worktree cwd

Open 💬 2 comments Opened May 22, 2026 by enerai

Hi Codex team,

I’ve been investigating a hook failure in the Codex Desktop app, and I believe there is an inconsistency in how project hooks are discovered and executed when the app is operating inside a linked Git worktree.

Summary

When Codex Desktop / app-server runs in a linked Git worktree, project hooks appear to be loaded from the primary checkout’s .codex/hooks.json, rather than from the current linked worktree’s own .codex/hooks.json.

However, the hook command itself is still executed with cwd set to the linked worktree.

This creates a mismatch between:

Hook config source:
/Users/<username>/<repository>/.codex/hooks.json

Hook execution cwd:
/Users/<username>/.codex/worktrees/c155/<repository>

As a result, hook configuration is sourced from the primary checkout, but any command-time repo-root resolution, such as git rev-parse --show-toplevel, resolves to the linked worktree.

Example

Given a hook command like this:

/Users/<username>/<repository>/.venv/bin/python "$(git rev-parse --show-toplevel)/.codex/hooks/subagent_dispatch_gate_prompt_hint.py"

At execution time, $(git rev-parse --show-toplevel) resolves to the linked worktree root:

/Users/<username>/.codex/worktrees/c155/<repository>

So although Codex loaded the hook configuration from:

/Users/<username>/<repository>/.codex/hooks.json

the actual script path becomes:

/Users/<username>/.codex/worktrees/c155/<repository>/.codex/hooks/subagent_dispatch_gate_prompt_hint.py

If the linked worktree’s current branch does not contain that hook script, the hook fails and blocks prompt submission:

can't open file .../.codex/hooks/subagent_dispatch_gate_prompt_hint.py: [Errno 2] No such file or directory

Local validation

I verified this against the backend bundled with the Codex Desktop app, not the Homebrew-installed CLI:

/Applications/Codex.app/Contents/Resources/codex app-server --listen stdio://
codex-cli 0.133.0-alpha.1

I called hooks/list from multiple working directories:

/Users/<username>/<repository>
/Users/<username>/.codex/worktrees/7c9a/<repository>
/Users/<username>/.codex/worktrees/d29f/<repository>
/Users/<username>/.codex/worktrees/f70e/<repository>

In all cases, the returned project hook sourcePath was:

/Users/<username>/<repository>/.codex/hooks.json

This also happened for linked worktrees that did not have their own .codex/hooks.json or .codex/hooks/ directory.

Source-level observation

From the Codex source, this appears to be intentional behavior in the app-server config loader rather than a transient cache issue.

Relevant files:

codex-rs/config/src/loader/mod.rs
codex-rs/hooks/src/engine/discovery.rs

The loader appears to special-case linked worktrees: the regular project-layer .codex directory remains associated with the current cwd / linked worktree, but the hook-specific hooks_config_folder is redirected to the primary checkout’s .codex directory.

Hook discovery then uses hooks_config_folder to load hooks.json.

The effective behavior is therefore:

project config layer dotCodexFolder = linked worktree .codex
project hooks config folder        = primary checkout .codex
hook execution cwd                 = linked worktree

This means hook discovery and hook execution operate against different roots.

Impact

This affects any project hook command that uses git rev-parse --show-toplevel, pwd, or other cwd-based logic to locate hook scripts or repo-local helper files.

The failure mode is especially easy to hit in Codex Desktop because the app creates or uses linked worktrees under:

~/.codex/worktrees/<id>/...

A user may reasonably expect that if the current linked worktree branch does not contain a .codex/hooks.json, then no project hook from that branch should run. Instead, app-server loads the hook from the primary checkout and executes it from the linked worktree, which can cause the hook to reference files that do not exist on the linked worktree branch.

Questions / suggestions

Could you confirm whether this is the intended design?

If linked worktrees are intentionally meant to reuse hooks from the primary checkout, it would be helpful for the documentation to make this contract explicit:

The project hook discovery root may differ from the hook execution cwd / repo root.

It may also be useful to expose the hook source location to hook commands via environment variables, for example:

CODEX_HOOK_SOURCE_PATH=/Users/<username>/<repository>/.codex/hooks.json
CODEX_HOOK_CONFIG_DIR=/Users/<username>/<repository>/.codex
CODEX_WORKTREE_ROOT=/Users/<username>/.codex/worktrees/c155/<repository>

That would let hook authors reliably distinguish between:

the repository / checkout that provided the hook configuration

and:

the linked worktree where the hook is being executed

Alternatively, if project hooks are expected to follow the current linked worktree branch, then hooks/list and hook discovery should probably read:

<current-linked-worktree>/.codex/hooks.json

rather than:

<primary-checkout>/.codex/hooks.json

One-line summary

Codex Desktop / app-server currently appears to load project hooks from the primary checkout when running inside a linked worktree, while executing the hook command from the linked worktree cwd. This causes git rev-parse --show-toplevel inside the hook command to resolve to the linked worktree, so the hook config and hook script path can point to different branches, leading to missing-file failures that block prompt submission.

View original on GitHub ↗

2 Comments

jshaofa-ui · 3 months ago

Proposed Solution: Linked Worktree Hook Discovery Fix

Root Cause

The app-server config loader special-cases linked worktrees: hook config is loaded from the primary checkout's .codex/hooks.json but executed from the linked worktree cwd. This causes git rev-parse --show-toplevel to resolve to the worktree, leading to missing-file failures.

Fix

  1. Worktree-Aware Discovery: Check worktree's own .codex/hooks.json first, fall back to primary
  2. Environment Variables: Expose CODEX_HOOK_SOURCE_PATH, CODEX_WORKTREE_ROOT to hook commands
  3. Script Path Resolution: Resolve $(git rev-parse --show-toplevel) using worktree root

Key Code Change

fn discover_hooks(cwd: &Path, repo_root: &Path) -> Result<HooksConfig> {
    let worktree_hooks = cwd.join(".codex").join("hooks.json");
    if worktree_hooks.exists() {
        return load_hooks_from(&worktree_hooks);
    }
    // Fall back to primary checkout
    load_hooks_from(&repo_root.join(".codex").join("hooks.json"))
}

Full solution: solutions/codex-23996-linked-worktree-hooks-fix.md

jshaofa-ui · 3 months ago

Solution: Project Hooks CWD Mismatch in Linked Worktrees

Root Cause

Project hooks in linked worktrees are loaded from the primary checkout's path but executed from the linked worktree's cwd. This causes:

  1. Hook scripts resolve relative paths against the wrong directory
  2. Environment variables (e.g., PROJECT_ROOT) point to the primary checkout
  3. Hook configuration files are read from the primary checkout, not the linked worktree

Fix

1. Detect linked worktrees and resolve hook paths correctly:

// In the hook loading code:
import { execSync } from 'child_process';

function getWorktreeInfo(): { isLinked: boolean; primaryPath: string; linkedPath: string } {
  try {
    // Check if we're in a linked worktree
    const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf-8' }).trim();
    const isLinked = gitDir.endsWith('.git') && !gitDir.includes('/.git');

    if (isLinked) {
      const primaryPath = execSync(
        'git -C ' + gitDir + ' config core.worktree',
        { encoding: 'utf-8' }
      ).trim();
      return {
        isLinked: true,
        primaryPath: primaryPath,
        linkedPath: process.cwd()
      };
    }
  } catch {
    // Not a git repo or git not available
  }
  return { isLinked: false, primaryPath: '', linkedPath: process.cwd() };
}

2. Resolve hook paths relative to the linked worktree, not the primary checkout:

function resolveHookPath(hookConfig: HookConfig, worktreeInfo: WorktreeInfo): string {
  if (worktreeInfo.isLinked && path.isAbsolute(hookConfig.path)) {
    // Replace primary checkout path with linked worktree path
    return hookConfig.path.replace(
      worktreeInfo.primaryPath,
      worktreeInfo.linkedPath
    );
  }
  return hookConfig.path;
}

3. Set correct environment variables for hook execution:

function getHookEnv(worktreeInfo: WorktreeInfo): NodeJS.ProcessEnv {
  return {
    ...process.env,
    PROJECT_ROOT: worktreeInfo.linkedPath,
    GIT_DIR: path.join(worktreeInfo.linkedPath, '.git'),
    GIT_WORK_TREE: worktreeInfo.linkedPath,
  };
}

Why This Works

  • Detects linked worktrees via git's worktree configuration
  • Resolves hook paths relative to the linked worktree's location
  • Sets correct environment variables so hooks execute in the right context
  • Works for both primary checkout and linked worktrees

Zero Competition

No other solution comments submitted for this issue.