CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-05-30Intermediate

Why git Says detected dubious ownership in repository — and How to Get Past It

An automation that ran fine yesterday suddenly dies on detected dubious ownership in repository. Here is what actually triggers it, the safe.directory fix, and how it differs from a real Permission denied.

Claude Code197git16safe.directorycontainerstroubleshooting87

One morning, an automation that had been running quietly for weeks stopped on a single line:

fatal: detected dubious ownership in repository at '/tmp/repos/my-app'
To add an exception for this directory, call:

	git config --global --add safe.directory /tmp/repos/my-app

As an indie developer who has shipped apps since 2014, I keep four Dolice sites updated daily through scheduled Claude Code runs, and the moment the sandbox switched to a different user, every git pull against the existing clone failed on this. The message is unusually friendly, yet the first time you see it the real question is: why did something that worked yesterday break today? Let me walk through it in the order I actually hit each piece.

What "dubious ownership" really means

This is not a git bug and it is not a corrupted repository. It fires when the user running git does not own the repository directory, and git deliberately refuses to continue as a safety measure.

The reason traces back to CVE-2022-24765. On a shared machine or a temporary directory like /tmp, if someone else drops a .git/config there and git reads it under your privileges, settings such as core.fsmonitor could be used to run arbitrary commands. So from Git 2.35.2 onward, touching a repository owned by another user stops immediately.

In my case the ownership told the whole story at a glance:

$ ls -ld /tmp/repos/my-app /tmp/repos/my-app/.git
drwxr-xr-x 7 nobody nogroup 4096 /tmp/repos/my-app
drwxr-xr-x 8 nobody nogroup 4096 /tmp/repos/my-app/.git
 
$ id -un
runner

The directory belongs to nobody, but the process is running as runner. When a sandbox's UID changes between one run and the next, this kind of mismatch is completely normal in containers and ephemeral environments.

The fastest fix: register a safe.directory

If you can vouch that the repository is trustworthy, the quickest path is exactly what the message suggests — add it as an exception:

git config --global --add safe.directory /tmp/repos/my-app

That whitelists just this directory, and your subsequent git pull or git status go through. The entry lands in the running user's global ~/.gitconfig under a [safe] section, so you can confirm it like this:

$ git config --global --get-all safe.directory
/tmp/repos/my-app

The detail that matters is --add. Without it, repeating git config --global safe.directory ... overwrites the previous value, which bites you the moment you handle more than one repository. Once I started registering each repo path with --add at the top of my automation scripts, this whole class of stumble disappeared.

Wildcards are handy, but mind the blast radius

If adding a path every time feels tedious, you can trust every repository with a wildcard:

git config --global --add safe.directory '*'

For a throwaway environment — a CI container that you fully control and rebuild on every run — this is pragmatic, and I use * in disposable build containers myself.

But avoid * on a shared development machine, or anywhere someone else's repository might appear. It removes the ownership check entirely and hands back the exact risk CVE-2022-24765 was meant to guard against. Whether the environment is disposable is, I think, the cleanest line to draw.

"dubious ownership" and "Permission denied" are not the same thing

This was the trap I fell into that same morning. After registering safe.directory, the error simply changed:

error: cannot open .git/FETCH_HEAD: Permission denied

It is tempting to read this as "my safe.directory setting did not take," but it is a different problem altogether:

  • detected dubious ownership — git is voluntarily stopping because the owner differs. A config (safe.directory) clears it.
  • Permission denied (when writing FETCH_HEAD and friends) — the OS file permissions genuinely will not let you write. No git config fixes this.

If a runner user tries to git pull into a directory owned by nobody, no amount of safe.directory entries will help — the OS rejects the write. The reliable check is to look at the actual permissions and confirm whether the write bit is yours:

$ touch /tmp/repos/my-app/.git/_wtest && echo OK || echo "NOT WRITABLE"
NOT WRITABLE

Once you know you cannot write, you are no longer in the "tweak a setting" phase. You are in the "fix the ownership, or move to a writable place" phase.

Root fixes that match the environment

To resolve the ownership mismatch permanently, pick whichever fits your situation.

If you have the rights to reclaim ownership, lining it up with the running user via chown is the textbook move:

sudo chown -R "$(id -un):$(id -gn)" /tmp/repos/my-app

If sudo is unavailable, or you are in a shared environment where you must not change ownership, the dependable choice is to rebuild somewhere you can definitely write. Because my scheduled runs periodically found /tmp owned by another user and unwritable, I switched to falling back to a writable spot under $HOME:

WORK="$HOME/repos/my-app"
if [ -d "$WORK/.git" ] && [ -w "$WORK/.git" ]; then
  cd "$WORK" && git pull --rebase origin main
else
  rm -rf "$WORK"
  git clone --depth 1 "$REPO_URL" "$WORK"
fi

Cloning as the same user that will eventually run git helps too. A Docker build that clones as root and then drops to a non-root user at runtime produces exactly this ownership split. Cloning after the USER switch, or slipping in a single chown, prevents it before it starts.

So the same morning does not repeat

At root, this error collapses to one question: who owns the file? Checking ownership and writability up front, before anything runs, is far faster than diagnosing a halt after the fact. I now keep these three lines at the top of my scripts:

git config --global --add safe.directory "$WORK"
[ -w "$WORK/.git" ] || WORK="$HOME/repos/$(basename "$WORK")"
echo "owner=$(stat -c '%U' "$WORK" 2>/dev/null) me=$(id -un)"

Register safe.directory to clear the ownership check, escape to a writable location if you cannot write, and finally log the owner next to yourself. That alone makes it obvious, when you open the log the next morning, what actually happened.

The more often a system runs, the more these quiet broken assumptions add up. I hope this saves someone else the same morning.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-05-27
When Claude Code's Bash Tool Hits Permission Denied on /tmp — A $HOME/repos Fallback Pattern
A practical look at why git clone inside Claude Code's sandboxed Bash sometimes fails with Permission denied on /tmp, and how a tiny $HOME/repos fallback keeps unattended schedules alive across four indie sites.
Claude Code2026-07-01
My Claude Code Hooks Stopped Firing After an Update — the Hyphenated Matcher Exact-Match Change in v2.1.195
In Claude Code v2.1.195, hook matchers containing a hyphen switched from partial match to exact match, silently disabling an existing PreToolUse hook. Here is how I isolated the cause and how to write matchers that won't break.
Claude Code2026-06-24
I Watched an Agent Try to Fix a File It Had Already Fixed — Stale Shallow Clones and Refreshing Before You Decide
An unattended agent tried to re-fix a file it had already fixed. The cause was a days-old shallow clone it kept reading. Here is how to detect that staleness numerically and re-clone only before decisions.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →