Cloud agent skipping custom git hooks

Where does the bug appear (feature/product)?

Background Agent (GitHub, Slack, Web, Linear)

Describe the Bug

Summary

Husky is a commonly used tool for Git hook management, but this issue is not specific to Husky—it will affect any tool that manages Git hooks via core.hooksPath. Cursor Cloud overrides core.hooksPath to its own dispatcher directory (~/.cursor/agent-hooks//), but does not reliably interoperate with external hook managers. This creates a conflict where either Cursor hooks or external hooks are skipped depending on which system last modified core.hooksPath.

Environment

Cursor Cloud (agent-enabled workspace)

Husky (via “prepare”: “husky” in package.json)

Git hooks: pre-commit, pre-push, etc.

Observed Behavior

Cursor sets:

core.hooksPath = ~/.cursor/agent-hooks//

This overrides any existing hook manager (e.g. Husky), which typically sets:

core.hooksPath = .husky/_

Cursor attempts to chain to the original hooks path via:

.cursor-original-hooks-path

But this is a directory that only contains sample files as Husky uses core.hooksPath as well

Result: dispatcher is never invoked for some hooks (e.g. pre-push)

Key Problems

  1. Cursor overrides external hook managers (e.g. Husky), causing critical checks to be skipped

When Cursor sets core.hooksPath, it takes exclusive control of Git hooks

This results in commit/push checks (tests, linting, policy enforcement) being silently skipped. These checks are especially important for gating AI-generated code, so skipping them is high risk.

  1. Husky taking over Cursor hooks

Husky runs via “prepare”: “husky” during npm install. If the agent ever runs this command during its lifecycle, it resets.

core.hooksPath = .husky/_

After this, Cursor’s dispatcher is no longer used, and Cursor-specific hooks (.cursor) do not run at all

Result: Cursor functionality becomes inconsistent depending on whether npm install has been executed

Steps to Reproduce

Create a simple node project with husky installed, and a single pre-push hook

Expected Behavior

I would expect Cursor to check that core.hooksPath has been set and act accordingly with forwarding. I think this check may happen, but it likely runs before the agent setup script. In our case, this is when we run npm install, which is when the core.hooksPath

Operating System

MacOS

Version Information

Cloud Agent

Does this stop you from using Cursor

No - Cursor works, but with this issue

If it’s useful, here an ensure-hooks.sh script that makes everything work together. This has to run AFTER husky


#!/usr/bin/env sh

# Ensures Husky git hooks work when Cursor Cloud overrides core.hooksPath

# with its own agent-hooks dispatcher. Idempotent — safe to call repeatedly.

#

# Called automatically from the "prepare" npm script after `husky`.

set -e

REPO_ROOT=$(git rev-parse --show-toplevel)

HUSKY_DIR="$REPO_ROOT/.husky/_"

HOOKS_DIR=$(git config --get core.hooksPath 2>/dev/null || true)

# --- Cursor Cloud agent-hooks fix ---

# Cursor Cloud replaces core.hooksPath with its own dispatcher that chains

# to the original hooks.  Detect the agent-hooks directory even when

# core.hooksPath has been accidentally overwritten (e.g. by `npx husky`

# re-running in prepare, which blindly sets core.hooksPath = .husky/_).

AGENT_HOOKS_ROOT="$HOME/.cursor/agent-hooks"

AGENT_HOOKS_DIR=""

if [ -d "$AGENT_HOOKS_ROOT" ]; then

  for d in "$AGENT_HOOKS_ROOT"/*/; do

    if [ -f "${d}.dispatcher" ]; then

      AGENT_HOOKS_DIR="${d%/}"

      break

    fi

  done

fi

# If agent-hooks exist but core.hooksPath doesn't point to them, restore it.

if [ -n "$AGENT_HOOKS_DIR" ] && [ "$HOOKS_DIR" != "$AGENT_HOOKS_DIR" ]; then

  git config core.hooksPath "$AGENT_HOOKS_DIR"

  HOOKS_DIR="$AGENT_HOOKS_DIR"

  echo "[ensure-hooks] Restored core.hooksPath to $AGENT_HOOKS_DIR"

fi

# Nothing more to do if we're not using agent-hooks

case "$HOOKS_DIR" in

  *agent-hooks*) ;;

  *) exit 0 ;;

esac

if [ ! -d "$HOOKS_DIR" ] || [ ! -f "$HOOKS_DIR/.dispatcher" ]; then

  exit 0

fi

ORIG_PATH_FILE="$HOOKS_DIR/.cursor-original-hooks-path"

# Fix 1: Point .cursor-original-hooks-path to Husky's hook handler directory

if [ -d "$HUSKY_DIR" ]; then

  CURRENT_ORIG=$(cat "$ORIG_PATH_FILE" 2>/dev/null || true)

  if [ "$CURRENT_ORIG" != "$HUSKY_DIR" ]; then

    echo "$HUSKY_DIR" > "$ORIG_PATH_FILE"

    echo "[ensure-hooks] Updated original hooks path to $HUSKY_DIR"

  fi

fi

# Fix 2: Ensure symlinks exist for all Husky hooks that have user scripts

for hook_script in "$REPO_ROOT"/.husky/pre-push "$REPO_ROOT"/.husky/pre-commit "$REPO_ROOT"/.husky/commit-msg; do

  if [ -f "$hook_script" ]; then

    hook_name=$(basename "$hook_script")

    target="$HOOKS_DIR/$hook_name"

    if [ ! -e "$target" ] || [ "$(readlink "$target" 2>/dev/null)" != ".dispatcher" ]; then

      ln -sf .dispatcher "$target"

      echo "[ensure-hooks] Created $hook_name hook symlink"

    fi

  fi

done

Hey, this is a really well-written bug report, and the workaround script is great. It’ll definitely help other users hitting the same issue.

This is a real bug. The one-time capture of the original hooks path, combined with the mutual core.hooksPath override between Cursor and Husky, or any external hook manager, is a clear design gap. I’ve flagged this with the team.

If you’re running into this too, the ensure-hooks.sh script Robin shared in the thread is a solid workaround. Add it to your prepare script after husky, and it’ll keep both systems working together.