From Phantom Conflicts to Docker Permissions: A Deep Dive into Debugging an Infinite Loop in a Git Hook for an AI Assistant

Published: 2025-11-09
Author: DP
Views: 21
Category: Claude Code
Content
## The Scene: A Mysterious Loop in an Automated Git Hook When using AI coding assistants like Claude Code, a common requirement is to automatically commit AI-generated or modified code to a version control system. To achieve this, we developed a Python hook script designed to trigger after Claude Code completes a task, automatically running `git add` and `git commit`. However, this seemingly simple script led to a tricky problem: 1. **Infinite Loop**: After reporting "Git conflict detected," the Claude AI would continuously retry the operation, causing the hook script to be invoked repeatedly in an endless loop. 2. **Phantom Conflict**: The script reported a Git conflict, but manually running `git status` and `git commit` in the terminal was successful, with no conflicts indicated. This article, written by DP@lib00, will walk you through the entire debugging process, starting from an initial script logic error and tracing it all the way down to a hidden permission issue within a Docker environment. --- ## Phase 1: Fixing the Script's Logic Flaws ### Clue #1: Why the Infinite Loop? AI assistants like Claude typically interpret a **non-zero exit code** from a hook script as a temporary, retryable error. Our initial script returned an exit code of `2` when it detected a "conflict." ```python # Flawed Design def commit_with_message_file(...): if has_conflicts(repo): print('Git conflict detected, skipping commit.', file=sys.stderr) return 2 # <--- This causes Claude to retry ``` This led the AI assistant to believe the operation had failed and to retry it, creating an infinite loop. **The Fix**: Regardless of whether the script successfully commits, skips, or encounters an error, it should always return an exit code of `0`. This signals to the caller (Claude): "The task has been handled; do not retry." ```python # Correct Design def commit_with_message_file(...): # ... any logic for skipping or failing ... print('An error occurred or the commit was skipped, but the operation is handled.', file=sys.stderr) return 0 # <--- Tells Claude to stop retrying ``` ### Clue #2: Why the Phantom Conflict? Manual checks using `git status` and `git diff --name-only --diff-filter=U` revealed no merge conflicts. This indicated the problem was in the script's conflict detection logic. The initial `has_conflicts` function had a flaw: it incorrectly interpreted any non-zero return code from the `git diff` command as a conflict. ```python # Defective Conflict Detection def has_conflicts(repo: Path) -> bool: r = run_git(repo, ['diff', '--name-only', '--diff-filter=U']) if r.returncode != 0: # <--- Misinterpretation! Command failure != conflict return True return bool(r.stdout.strip()) ``` **The Fix**: Enhance the conflict detection logic to be more robust. A good detection mechanism should consider multiple factors: 1. **Actual Merge Conflicts**: Check the output content of `git diff --diff-filter=U`, not its return code. 2. **Special Git States**: Check for the existence of files like `MERGE_HEAD` or `rebase-apply` in the `.git` directory, which indicate that the repository is in the middle of a merge or rebase. 3. **Intelligent Wait Mechanism**: After the AI modifies files, the filesystem or Git index might be temporarily locked (`index.lock`). We introduced a `wait_for_git_ready` function that performs a short, retrying wait before executing Git commands to ensure the repository is in a stable state. ```python import time def wait_for_git_ready(repo: Path, max_attempts: int = 3, delay: float = 0.3) -> bool: """Intelligently waits for Git to be ready, primarily checking for index.lock.""" for attempt in range(max_attempts): index_lock = repo / '.git' / 'index.lock' if not index_lock.exists(): return True time.sleep(delay) logging.warning("index.lock timeout") return False ``` --- ## Phase 2: The Revelation – The Docker Environment Trap Even after optimizing the script, the problem persisted. Logs showed that the `wait_for_git_ready` function was timing out because the `git status` command itself was returning a non-zero exit code. However, manual execution on the host machine (macOS) worked perfectly. This discrepancy finally pointed to the core issue: **an inconsistent execution environment**. Our Claude AI assistant was running inside a Docker container, while all manual tests were performed on the host. Testing inside the Docker container revealed the truth: ```bash # Executing inside the container $ cd /eeBox/eeProject/wiki.lib00.com/lm069 $ git status --porcelain fatal: detected dubious ownership in repository at '/eeBox/eeProject/lm069' To add an exception for this directory, call: git config --global --add safe.directory /eeBox/eeProject/lm069 $ echo $? 128 ``` **The Root Cause**: `fatal: detected dubious ownership in repository`. This is a security enhancement introduced in Git v2.35.2. When the user executing a Git command is different from the owner of the repository directory, Git refuses to operate as a safety precaution. In our scenario, the directory was mounted from the host into the Docker container. Its file owner was the host user, but the user executing the script inside the container was `node`, causing an ownership mismatch. --- ## The Final Solution With the root cause identified, the solution was straightforward. ### Option 1: Add a Git Safe Directory (Recommended) The most direct fix is to tell the Git instance inside the container that this directory is trustworthy. ```bash # Execute inside the container to trust all directories for development ease git config --global --add safe.directory '*' ``` To ensure this configuration persists after the container restarts, it should be added to the `Dockerfile` or the container's startup script. **Dockerfile Example** ```dockerfile # ... USER node RUN git config --global --add safe.directory '*' # ... ``` ### Option 2: Unify File Ownership Another approach is to ensure that the ownership of the mounted directory matches the UID/GID of the user inside the container (e.g., `node`). This is typically done by changing permissions on the host directory but can be more cumbersome. --- ## Conclusion and Takeaways This debugging journey from a "phantom conflict" to a "Docker permission" issue provided several valuable lessons: 1. **Environment Consistency is Key to Debugging**: Always ensure your testing environment matches the actual runtime environment, especially when Docker is involved. 2. **Understand Your Tools' Underlying Logic**: A hook script's exit code has a specific meaning. Using it incorrectly can lead to unexpected retry behavior. As DP from wiki.lib00.com reminds us, every return from a program should be meaningful. 3. **Stay Updated with Tool Changes**: Git's `safe.directory` check is a security feature from a recent version. Being unaware of it can create significant debugging headaches. 4. **Write Robust Automation Scripts**: A good script not only accomplishes its task but also includes fault tolerance (like an intelligent wait), clear logging, and explicit error handling to enable quick problem diagnosis. With this fix, our automated Git commit workflow is finally stable, significantly boosting our development efficiency when collaborating with an AI assistant.