Skip to content

hook install/status/uninstall crash on git worktrees (regression of #516) #865

Description

@etholpalmer

Symptom

graphify hook install and graphify hook status both crash with NotADirectoryError when run from a linked git worktree (i.e. a directory created via git worktree add).

$ graphify hook install
Traceback (most recent call last):
  File ".../graphify/__main__.py", line 1381, in main
    print(hook_install(Path(".")))
  File ".../graphify/hooks.py", line 246, in install
    hooks_dir = _hooks_dir(root)
  File ".../graphify/hooks.py", line 201, in _hooks_dir
    d.mkdir(exist_ok=True)
NotADirectoryError: [Errno 20] Not a directory: '/path/to/worktree/.git/hooks'

graphify hook status and graphify hook uninstall fail identically.

Regression of #516

This is the same crash as #516 ("无法在 git worktree 项目运行 hooks 命令"), which was closed in April 2026 with the comment:

Fixed in v0.4.32: _hooks_dir() now uses git rev-parse --path-format=absolute --git-path hooks instead of git config core.hooksPath. Git resolves this correctly for both normal repos and worktree checkouts, pointing to the shared hooks directory in the main repo's .git/hooks. Includes regression tests for install/uninstall from a worktree.

However, the current v7 (default) branch source shows _hooks_dir still uses the broken path-join approach — no git rev-parse call:

https://github.com/safishamsi/graphify/blob/v7/graphify/hooks.py

def _hooks_dir(root: Path) -> Path:
    """Return the git hooks directory, respecting core.hooksPath if set (e.g. Husky)."""
    try:
        cfg = configparser.RawConfigParser()
        cfg.read(root / ".git" / "config", encoding="utf-8")
        # ... core.hooksPath handling (only works when .git is a directory) ...
    except (configparser.Error, OSError) as exc:
        ...
    d = root / ".git" / "hooks"
    d.mkdir(exist_ok=True)   # <-- NotADirectoryError here when .git is a file
    return d

Either the v0.4.32 fix was reverted, or it never landed on the release branch and only existed in a feature branch. Bug reproduces on the current pypi release (graphifyy 0.7.13) and is still present in v7 HEAD as of today.

Reproduction

git init main-repo && cd main-repo
echo "hi" > a.txt && git add . && git commit -m init
git worktree add ../wt
cd ../wt
graphify hook install   # crashes
graphify hook status    # also crashes

In a linked worktree, .git is a regular file containing gitdir: <main-repo>/.git/worktrees/<name>, not a directory. _git_root() matches it via (parent / ".git").exists(), then _hooks_dir() does root / ".git" / "hooks" and the mkdir call fails.

Suggested Fix

Use git's own machinery to locate the hooks dir, which transparently handles worktrees:

import subprocess

def _hooks_dir(root: Path) -> Path:
    """Return the git hooks dir; handles worktrees via git rev-parse."""
    try:
        hooks_path = subprocess.check_output(
            ["git", "rev-parse", "--path-format=absolute", "--git-path", "hooks"],
            cwd=root,
            text=True,
        ).strip()
        p = Path(hooks_path)
        p.mkdir(parents=True, exist_ok=True)
        return p
    except subprocess.CalledProcessError:
        # Fall back to the current logic for non-git or unusual cases
        d = root / ".git" / "hooks"
        d.mkdir(exist_ok=True)
        return d

git rev-parse --git-path hooks returns:

  • <repo>/.git/hooks in a normal repo
  • <main-repo>/.git/hooks from any linked worktree (the shared hooks dir, which is the correct location per git's design)
  • A custom path when core.hooksPath is set

_git_root() should also be updated to use git rev-parse --show-toplevel rather than walking up looking for .git, but the _hooks_dir fix alone resolves the crash.

Workaround

For anyone hitting this now, install hooks manually to the common dir:

from graphify import hooks
from pathlib import Path
import subprocess
common = Path(subprocess.check_output(["git", "rev-parse", "--git-common-dir"], text=True).strip())
hooks_dir = common / "hooks"
print(hooks._install_hook(hooks_dir, "post-commit", hooks._HOOK_SCRIPT, hooks._HOOK_MARKER))
print(hooks._install_hook(hooks_dir, "post-checkout", hooks._CHECKOUT_SCRIPT, hooks._CHECKOUT_MARKER))

To uninstall manually: rm $(git rev-parse --git-common-dir)/hooks/post-commit $(git rev-parse --git-common-dir)/hooks/post-checkout.

Environment

  • macOS 25.3.0 (Darwin arm64)
  • Python 3.14.5 (pipx install)
  • graphifyy 0.7.13 (latest pypi: 0.7.19; bug also visible in v7 HEAD)
  • Worktree created via git worktree add

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions