GitLab CI Error Guide: 'detected dubious ownership' — Fix Git safe.directory
Fix 'detected dubious ownership in repository' in GitLab CI: add the build path to git safe.directory, align UID ownership, and unblock git on runners.
- #gitlab
- #ci-cd
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
Git refuses to operate on a repository whose working directory is owned by a different user than the one running git. In GitLab CI this surfaces the moment a job runs any git command against the build directory:
fatal: detected dubious ownership in repository at '/builds/team/app'
To add an exception for this directory, call:
git config --global --add safe.directory /builds/team/app
The job’s git step (describe, submodule, tag, diff, or even the runner’s own checkout) fails, and any script that shells out to git stops here. This is a Git security feature (CVE-2022-24765), not a GitLab bug.
Symptoms
- A git command fails with
detected dubious ownership in repository at '<path>'. - The failure started after a base-image upgrade to a newer Git version.
- The job runs git as a different user (e.g.,
rootin the container) than the one that owns/builds/...(or vice versa). - Tooling that internally calls git —
git describe, semantic-release,setuptools_scm, submodule init — fails even though your explicit script looks fine. - The same repo works on an older runner image but not a rebuilt one.
Common Root Causes
- UID mismatch between checkout and job — the runner (or a previous job) created
/builds/...as one UID; the job image runs git as another. - Container running as a non-root user while the build dir is owned by root, or the reverse.
- Reused build directory across images — a cached/persistent
/buildsvolume owned by a different UID than the current image’s user. - Newer Git enforcing the check — Git 2.35.2+ hardened ownership checks; an image upgrade turned a previously-silent mismatch into a fatal.
- Docker volume/bind mount ownership — a mounted host path owned by a UID that doesn’t match the container user.
- Rootless/DinD user namespacing — remapped UIDs make the checkout appear owned by an unexpected user inside the container.
Diagnostic Workflow
Confirm the ownership mismatch by comparing who owns the dir to who runs git:
ownership-debug:
script:
- id # UID/GID running the job
- ls -ld "$CI_PROJECT_DIR" # owner of the build directory
- git status || true # reproduces the dubious-ownership fatal
The direct fix is to mark the build path as safe for git in the job:
build:
before_script:
- git config --global --add safe.directory "$CI_PROJECT_DIR" # exact path
script:
- git describe --tags
To cover reused/generic runner paths, allow all directories (acceptable on ephemeral CI where the runner controls the environment):
build:
before_script:
- git config --global --add safe.directory '*' # trust all — CI-only convenience
script:
- ./release.sh
Alternatively, align ownership so the check passes without exceptions:
fix-ownership:
before_script:
- chown -R "$(id -u):$(id -g)" "$CI_PROJECT_DIR" # make git's user own the repo
script:
- git status
Example Root Cause Analysis
A release job that generated a version string with git describe began failing on every pipeline right after the team bumped their build image from Debian 11 to Debian 12:
release:
image: node:20-bookworm # newer image => newer Git with hardened ownership check
script:
- VERSION=$(git describe --tags) # <-- fatal: detected dubious ownership
- npm run release -- --version "$VERSION"
The runner checked out the repo into /builds/team/app as one UID (the runner’s helper), but the node:20-bookworm image ran the script as a different user. The older Debian 11 Git had not enforced the ownership check; Debian 12’s newer Git did, turning the long-standing UID mismatch into a fatal error. The fix added the build path to safe.directory before any git use:
release:
image: node:20-bookworm
before_script:
- git config --global --add safe.directory "$CI_PROJECT_DIR"
script:
- VERSION=$(git describe --tags)
- npm run release -- --version "$VERSION"
Because the failure was really a UID mismatch surfaced by a Git upgrade, the team also standardized the runner’s FF_.../user settings so checkout and job ran as the same UID, and added the safe.directory line to their shared CI template so future images inherit the fix.
Prevention Best Practices
- Add
git config --global --add safe.directory "$CI_PROJECT_DIR"to a sharedbefore_script/template so every job that touches git inherits it. - On ephemeral CI runners you control,
safe.directory '*'is an acceptable blanket fix; avoid it on shared multi-tenant hosts where trust boundaries matter. - Keep the checkout UID and the job image’s user aligned;
chownthe build dir to the job user when they must differ. - Expect ownership fatals after Git version bumps (2.35.2+) and add the safe.directory config proactively when upgrading base images.
- For persistent/reused
/buildsvolumes, ensure the volume UID matches the images that use it. - Bake the
safe.directorysetting into custom CI images (via a global gitconfig) so downstream jobs don’t each have to set it.
Quick Command Reference
# Who runs git vs. who owns the repo?
id
ls -ld "$CI_PROJECT_DIR"
# Direct fix — trust the exact build path
git config --global --add safe.directory "$CI_PROJECT_DIR"
# CI-only blanket trust (ephemeral runners you control)
git config --global --add safe.directory '*'
# Align ownership instead of adding an exception
chown -R "$(id -u):$(id -g)" "$CI_PROJECT_DIR"
# Bake into a custom image's global gitconfig
git config --system --add safe.directory '*'
Conclusion
detected dubious ownership in repository is Git’s security check firing because the build directory is owned by a different user than the one running git — usually a UID mismatch between the runner’s checkout and the job image, often surfaced by a Git version bump. Add the build path to git safe.directory (or align ownership with chown), put it in a shared CI template so every job inherits it, and standardize checkout/job UIDs so the mismatch doesn’t recur after your next base-image upgrade.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.