DEV Community

Cover image for 'Dubious Ownership': The 2022 Git CVE Behind the Error Your Agent's Container Hits
Gabriel Anhaia
Gabriel Anhaia

Posted on Edited on AI-assisted

'Dubious Ownership': The 2022 Git CVE Behind the Error Your Agent's Container Hits


Claude Code rejects --dangerously-skip-permissions when it's launched as root, per its dev container docs, so the container you build for an agent that runs without permission prompts uses a non-root user, say node at uid 1000. Your host account is uid 1001, and you start the container with plain docker run, the repo bind-mounted from the host. The agent's first git status comes back with this:

fatal: detected dubious ownership in repository at '/work'
To add an exception for this directory, call:

    git config --global --add safe.directory /work
Enter fullscreen mode Exit fullscreen mode

That is the message template in Git's setup.c, with /work as the path. The fix it suggests is one line. The tempting move is to go one step further, paste safe.directory '*' into a Dockerfile or your own ~/.gitconfig, and never see the error again.

That also switches off the protection. The check is there because of a real 2022 vulnerability, and the situation it guards against is the one you build when you give an agent its own user or container.

Reproduce it

On a Linux host, file ownership is a number. A bind mount shows the container the same number the host has. Your repo is owned by your uid, 1001 in the example above, and git inside the container checks it against its own. The quickest way to see a mismatch needs no custom user at all: Docker runs containers as root (uid 0) by default, and 0 isn't 1001, so git stops.

The alpine/git image runs git with whatever arguments you pass:

mkdir -p ~/code/demo && cd ~/code/demo
git init -q
docker run --rm -v "$PWD":/git -w /git \
  alpine/git status
Enter fullscreen mode Exit fullscreen mode

I haven't run this exact command myself. What git checks is in its source: is_path_owned_by_current_uid calls lstat on the path and compares the file's owner with geteuid(). When git runs as root it also accepts the uid in SUDO_UID, which sudo sets and docker run does not. Docker Desktop handles bind mounts its own way, so on macOS or Windows you may not see the error at all.

The non-root user from the opening fails the same way: node at 1000 inside the container, files owned by 1001 on the host, same message.

C:\.git and the 2022 fix

The check comes from CVE-2022-24765. When you run git in a folder, it walks up the parent directories looking for a .git. On a shared machine, someone else could create one above you. The Git 2.30.3 release notes give the examples: a repository in C:\.git, on a mounted network drive, or in a scratch space.

That matters because some config values run programs. GitHub's announcement names core.fsmonitor, which makes git execute a command. Plant a C:\.git\config with that value, and every user who runs git in a folder under C:\ that isn't its own repo runs your command. They don't even have to type git. The release notes point out that a Git-aware shell prompt running git status, or opening the folder in VS Code or Atom, was enough.

A black silhouette of a developer walking down a staircase of folders, unaware of an orange trapdoor marked with a git branch symbol planted in the folder above them
CVE-2022-24765: a .git planted in a parent folder could run its owner's commands when you ran git below it.

The timeline, from Git's own records:

  • March 2, 2022: the owner check for the top-level directory is written, as commit 8959555, authored by Johannes Schindelin (the commit message of the later fix gives the date).
  • April 12, 2022: Git 2.35.2 ships with the fix, along with backports to 2.30.3, 2.31.2, 2.32.1, 2.33.2 and 2.34.2. Git now refuses a repository owned by someone else unless it's listed in safe.directory. The error back then read unsafe repository ('...' is owned by someone else).
  • July 12, 2022: Git 2.37.1 and seven maintenance releases fix CVE-2022-29187. The April fix was incomplete, and the 2.30.5 notes describe the gap: it checked the worktree but not the .git directory or a .git file pointing to one. The patch extends the check to all three, gives a privilege-escalation example using a shared /tmp, and replaces the old wording with "detected dubious ownership in repository".

Vendor docs followed. Atlassian's article lists both wordings. GitHub's actions/checkout gained a set-safe-directory input, on by default, which runs git config --global --add safe.directory <path> for the one workspace path.

What safe.directory '*' turns off

The git-config docs put it plainly: "To completely opt-out of this security check, set safe.directory to the string *." Every repository on the machine is then treated as safe, whoever owns it.

Two details in the same docs matter for agents. safe.directory is only read from protected configuration, meaning system, global and command scope, so a repo can't vouch for itself. And a value ending in /* allows every repository under that directory, which is a middle ground but still broader than one path.

In a throwaway container that only ever mounts one repo, * costs you little. There is no second user inside to plant anything. The damage comes when it leaks out of there: into your host ~/.gitconfig because the error showed up once, into a base image the whole team builds on, or onto a shared CI runner.

A second OS user for the agent, a shared box you reach over SSH, a container user writing into a folder your host user also works in: each is the multi-user machine the CVE was written for. The less-trusted user is the agent, and Anthropic's own docs warn that, run with --dangerously-skip-permissions, a dev container doesn't stop a malicious project from exfiltrating anything accessible inside it. If the agent's user can create a .git with core.fsmonitor in a directory above where you later run git, the ownership check is what keeps that command from running as you. With * in your global config, nothing does.

Three fixes that keep the check on

Match the uid. If git runs as the user who owns the files, there's nothing to fix. With plain Docker, pass your own ids. --user takes uid:gid, per the Docker run reference:

docker run --rm -v "$PWD":/git -w /git \
  --user "$(id -u):$(id -g)" \
  alpine/git status
Enter fullscreen mode Exit fullscreen mode

Dev containers do this for you on Linux. The spec's updateRemoteUserUID rewrites the container user's UID/GID "to match the local user's UID/GID to avoid permission problems with bind mounts," when a remoteUser or containerUser is set, and it defaults to true.

Matching the uid keeps the check on, but git can no longer tell the agent's files from yours, so on that mount the container is the boundary. The check never covered your repo's own .git anyway: you own it, and an agent with write access to the bind mount can add a core.fsmonitor value or a hook there. Look at any .git/config or .git/hooks change the agent made before you run git on the host.

List one path, not all of them. When the uids can't match, name the mount point. The command scope counts as protected, so -c works for a single run:

docker run --rm -v "$PWD":/git -w /git \
  alpine/git -c safe.directory=/git status
Enter fullscreen mode Exit fullscreen mode

An agent runs git many times, so for a whole session pass it as environment variables. GIT_CONFIG_COUNT and the matching GIT_CONFIG_KEY_<n> and GIT_CONFIG_VALUE_<n> are command scope too, per the git-config docs:

docker run --rm -it -v "$PWD":/work -w /work \
  -e GIT_CONFIG_COUNT=1 \
  -e GIT_CONFIG_KEY_0=safe.directory \
  -e GIT_CONFIG_VALUE_0=/work \
  your-agent-image
Enter fullscreen mode Exit fullscreen mode

Bake the path into the image. If every container mounts the repo at the same place, one line in the Dockerfile covers it, and it lives in the image's system config, so your host config never sees it:

RUN git config --system --add safe.directory /work
Enter fullscreen mode Exit fullscreen mode

If someone already put safe.directory=* in the system config, the git-config docs say to start your own list with an empty value. That resets it, and the paths you add after it are the only ones trusted.

Worktrees make the mount trickier

Tools that run agents in parallel, Hermes included, often give each task its own git worktree. A linked worktree has a .git file, not a directory, and the git-worktree docs say it points back to the main repository's .git/worktrees/<name> folder. By default that pointer is an absolute host path. Mount only the worktree into a container and the path it names doesn't exist in there.

Two ways around it. Mount the main repository too, at the same path it has on the host. Or link worktrees with relative paths: Git 2.48 added this, set with worktree.useRelativePaths or --relative-paths. The docs warn it enables the extensions.relativeWorktrees extension, which older Git versions can't read. Remember the July 2022 fix as well: the ownership check now covers the worktree, the .git directory and the .git file, so all three have to belong to the container's user, or the worktree path goes in safe.directory.

I build Hermes IDE, so weigh this part with that in mind. Hermes starts each task on its own hermes/<task> branch in its own worktree, and every agent runs in a real terminal on your machine, as you. The agent's git and your git are the same user there, so the check never fires. The boundary comes in when you add one: an SSH session to a shared box (Hermes supports those), or a container you start from a plain shell session in the task's worktree. That's when the three fixes above apply.

The Hermes Review Desk Worktrees tab listing one worktree each for orders-api, web-app and orders-types, with branch, task, age and disk size
The Review Desk's Worktrees tab: one Hermes worktree each for orders-api, web-app and orders-types under ~/code, with each worktree's hermes/ branch, task, age and disk size, and the free disk space.

Check what you already trust

Before you add anything, see where a * might already be hiding. git config --show-origin --get-all safe.directory prints every entry with the file it came from. Entries in a repo's local config show up in that list too, but git ignores them for this check, per the git-config docs. A * in your global or system file is the one to replace with a path.


If this was useful

Giving an agent its own user, container or worktree is a guardrail, and every guardrail has an edge like this one where it rubs against older security assumptions. The AI Agents Pocket Guide covers the patterns behind autonomous agents, including how much autonomy to grant and where to put the guardrails. It's one of 8 short books in the Pocket Guides for Developers series, on agents, RAG, prompts, observability, events, databases and system design.

If you run several agents on one repo, Hermes IDE is free, source-available (BSL 1.1), on macOS, Windows and Linux, and on GitHub. It's not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor.

AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs

Top comments (0)