Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 9 min read

GitLab CI Error Guide: 'fatal: could not read Username for https://gitlab.com' Job Token Clone Failures

Fix GitLab CI's 'could not read Username' and 'terminal prompts disabled' when cloning a private dependency repo: use CI_JOB_TOKEN, url.insteadOf, and submodule strategy.

  • #gitlab-cicd
  • #troubleshooting
  • #errors
  • #authentication

Exact Error Message

A job that clones or fetches another repository — a dependency, a submodule, or a private module — dies with one of these in the log:

Cloning into 'shared-lib'...
fatal: could not read Username for 'https://gitlab.com': No such device or address
fatal: could not read Username for 'https://gitlab.com': terminal prompts disabled

The wording differs by Git version (No such device or address vs terminal prompts disabled), but the cause is identical: Git wanted to prompt for credentials, the CI environment has no TTY, and GIT_TERMINAL_PROMPT=0 (set by the runner) turned the prompt into a hard failure.

What the Error Means

GitLab automatically clones the current project for you using the per-job CI_JOB_TOKEN. That credential is scoped to that one project. The moment your script reaches out to a different repository over https://git clone, git submodule update, go mod download, pip install git+https://..., composer install — Git sees a bare URL with no embedded credentials, falls back to its interactive credential helper, finds no terminal, and aborts.

It is not a network problem and not a permissions problem (yet). It is a missing-credentials problem: nothing in the request told GitLab who is asking.

Common Causes

  1. Cloning another private repo without credentials. git clone https://gitlab.com/group/shared-lib.git inside a job has no token attached.
  2. A bare HTTPS URL instead of the job-token form. Hard-coded https://gitlab.com/... URLs in scripts, go.mod replace directives, or requirements.txt.
  3. Submodules over HTTPS. .gitmodules pins absolute https://gitlab.com/... URLs and GIT_SUBMODULE_STRATEGY pulls them with no auth.
  4. Package managers pulling private repos. Go modules, pip VCS installs, Composer VCS repos, and npm git dependencies all shell out to Git over HTTPS.
  5. A deploy token or PAT is actually required. When the source is in another group, or you need write access, CI_JOB_TOKEN may not be enough and a deploy token / Project Access Token is the right credential.

How to Reproduce the Error

Add a job that clones a second private project with a plain URL:

build:
  stage: build
  image: alpine/git
  script:
    - git clone https://gitlab.com/acme/shared-lib.git
    - ls shared-lib

The current project clones fine (GitLab injected its token), but the explicit git clone of shared-lib fails:

$ git clone https://gitlab.com/acme/shared-lib.git
Cloning into 'shared-lib'...
fatal: could not read Username for 'https://gitlab.com': terminal prompts disabled

Diagnostic Commands

Confirm the token exists and the prompt is disabled, and inspect the URL Git is actually using:

# In a job: confirm the predefined variables are present (never echo the token itself)
echo "user=${CI_JOB_TOKEN:+set}"        # prints "user=set" without leaking it
echo "GIT_TERMINAL_PROMPT=$GIT_TERMINAL_PROMPT"   # runner sets this to 0
git config --list --show-origin | grep -i url       # any insteadOf rewrite?
cat .gitmodules                                      # absolute https submodule URLs?
user=set
GIT_TERMINAL_PROMPT=0

If .gitmodules contains url = https://gitlab.com/acme/..., that absolute URL bypasses any credentials GitLab set up for the main clone.

Step-by-Step Resolution

1. Clone with the job token in the URL

The job token authenticates as the synthetic user gitlab-ci-token:

build:
  stage: build
  image: alpine/git
  script:
    - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/acme/shared-lib.git

2. Rewrite all HTTPS URLs once with url.insteadOf

Instead of editing every URL (handy for go mod, pip, Composer), rewrite the host globally at job start:

before_script:
  - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"

Now any tool that calls Git for https://gitlab.com/... transparently gets the token. For Go modules also export GOPRIVATE and GONOSUMCHECK as appropriate:

variables:
  GOPRIVATE: gitlab.com/acme

3. Fix submodules with relative paths + strategy

Prefer relative submodule paths in .gitmodules so they inherit the parent’s credentials:

[submodule "shared-lib"]
  path = shared-lib
  url = ../shared-lib.git

Then let the runner clone them:

variables:
  GIT_SUBMODULE_STRATEGY: recursive
  GIT_SUBMODULE_FORCE_HTTPS: "true"

Relative URLs resolve against the parent project’s origin, so the job token GitLab already injected covers them — no hard-coded host.

4. Use a deploy token or PAT when the job token isn’t enough

For cross-group access or write operations, create a deploy token (Settings > Repository > Deploy tokens) and pass it as masked CI/CD variables:

before_script:
  - git config --global url."https://${DEPLOY_USER}:${DEPLOY_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"

Prevention and Best Practices

  • Never hard-code https://gitlab.com/... in scripts, go.mod, or .gitmodules; use relative submodule URLs and a single url.insteadOf rewrite in before_script.
  • Always authenticate as gitlab-ci-token:${CI_JOB_TOKEN} for read access to sibling projects — it expires with the job, so nothing leaks.
  • Mask CI_JOB_TOKEN-derived values and never echo the raw token; use the ${VAR:+set} trick to confirm presence.
  • Set GIT_SUBMODULE_STRATEGY: recursive plus GIT_SUBMODULE_FORCE_HTTPS: "true" rather than fighting SSH key distribution in CI.
  • The free incident assistant can turn a clone-failure log into a likely-cause-and-fix suggestion. More patterns live in the GitLab CI/CD guides.
  • remote: You are not allowed to download code from this project — once you do send the job token, a 403 means the source project hasn’t allowlisted yours under Token Access. That is the next error you hit after fixing this one.
  • Invalid CI config — a malformed before_script rewrite can break config parsing; see the Invalid CI config guide.
  • Host key verification failed — the SSH equivalent when you clone over git@ instead of HTTPS.

Frequently Asked Questions

Why does the main project clone but my second git clone fails?

GitLab injects CI_JOB_TOKEN only for the project that owns the pipeline. Any additional repo you clone is a plain, unauthenticated request unless you embed credentials yourself.

Is it safe to put CI_JOB_TOKEN in the clone URL?

Yes — it is the documented pattern. The token is short-lived (valid only for that job) and GitLab masks it in logs. Do not echo it or write it to a file that becomes an artifact.

Why does go mod download fail even though my own git clone works?

Go shells out to Git for https://gitlab.com/... and never sees your one-off URL. Use url.insteadOf so every Git invocation gets the token, and set GOPRIVATE so Go skips the public proxy and checksum database.

Should I use a deploy token instead of the job token?

Use the job token for read-only access to projects in the same scope — it is automatic and self-expiring. Reach for a deploy token or Project Access Token only when you need cross-group access, write access, or a credential that outlives the job.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

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.