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
runnerThe 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-appThat 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-appThe 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 WRITABLEOnce 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-appIf 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"
fiCloning 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.