CI/CD

fatal: could not read Username for 'https://gitlab.com': No such device or address

A CI job tried to authenticate to Git with no credentials and no terminal to prompt on. How to use CI_JOB_TOKEN, a deploy key, or SSH properly.

medium fix6 min read

the ci/cd error
fatal: could not read Username for 'https://gitlab.com': No such device or address

fatal: could not read Username for 'https://github.com': terminal prompts disabled

remote: HTTP Basic: Access denied
fatal: Authentication failed for 'https://gitlab.com/acme/private-lib.git/'

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Find which repository is being fetched without credentials

    grep -rn "url = " .gitmodules 2>/dev/null || echo "no submodules"

    The checkout of the project itself is authenticated by the runner. A second repository, usually a submodule or a dependency pulled during the build, is not, and that is almost always what fails.

  2. 2

    Rewrite HTTPS URLs to carry the job token

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

    insteadOf applies the credential to every matching URL, including ones inside .gitmodules and package manifests that you cannot easily edit. It is the least invasive fix.

  3. 3

    For submodules, let the checkout handle them

    echo "GIT_SUBMODULE_STRATEGY=recursive"

    Setting this variable makes GitLab's own checkout fetch submodules with the job's credentials already configured, rather than leaving a later git submodule update to authenticate on its own.

All 10 sections

Git needed a username and had no way to ask. There is no terminal in a CI job, so instead of prompting it fails with No such device or address, or on newer versions terminal prompts disabled.

The message is about the absence of a prompt. The real problem is the absence of a credential.

It is rarely the main checkout

GitLab's runner configures credentials for the project being built, so git clone of the project itself works. What fails is a second repository fetched during the job:

  • A submodule
  • A Go module from a private GitLab group
  • A pip, npm or Composer dependency with a git URL
  • A git clone in your own script
grep -rn "url = " .gitmodules
url = https://gitlab.com/acme/private-lib.git

That URL carries no credentials, so the fetch is anonymous, and a private repository refuses it.

Fix 1: insteadOf with CI_JOB_TOKEN

CI_JOB_TOKEN is injected into every job and is scoped to that job's lifetime. Rewrite matching URLs to carry it:

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

This applies to every https://gitlab.com/ URL Git encounters, including ones inside .gitmodules and inside tooling you do not control, which is why it is the least invasive option.

The username is literally gitlab-ci-token. It is not your username and not a variable.

For GitHub Actions the equivalent is:

- run: git config --global url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"

CI_JOB_TOKEN has to be allowed

Since GitLab 16, a job token from project A cannot read project B by default. You allowlist it explicitly.

In the target project: Settings → CI/CD → Job token permissions, add the source project to the allowlist.

Without that, the token authenticates and is then refused, which looks like this:

remote: HTTP Basic: Access denied

That is a different message from the one at the top of this page, and it means you got past the missing-credential problem and hit an authorisation one.

Fix 2: Let the runner fetch submodules

Simplest when submodules are the issue:

variables:
  GIT_SUBMODULE_STRATEGY: recursive
  GIT_SUBMODULE_FORCE_HTTPS: "true"

The runner fetches them as part of its checkout, with credentials already set up. GIT_SUBMODULE_FORCE_HTTPS rewrites git@ URLs in .gitmodules to HTTPS so the job token applies, which saves changing the file.

This only covers submodules. A dependency fetched by a package manager still needs Fix 1.

Fix 3: Deploy keys and SSH

For cross-project access that outlives a job, or another host entirely, a deploy key scoped to the one repository is better than a personal token.

Generate a key, add the public half as a deploy key on the target project, and put the private half in a masked, protected CI variable of type File:

before_script:
  - mkdir -p ~/.ssh && chmod 700 ~/.ssh
  - cp "$SSH_PRIVATE_KEY" ~/.ssh/id_ed25519
  - chmod 600 ~/.ssh/id_ed25519
  - ssh-keyscan gitlab.com >> ~/.ssh/known_hosts

Use a File-type variable rather than pasting the key into a normal one, which mangles newlines and produces invalid format errors that waste a lot of time.

ssh-keyscan at runtime trusts whatever answers. For anything sensitive, pin a known host key in the repository instead.

Never do this

- git clone https://user:${MY_PERSONAL_TOKEN}@gitlab.com/acme/lib.git

Two problems. A personal token carries your entire access, so the job can reach every project you can. And the URL with the token embedded is written into .git/config and frequently into job logs, where masking does not always catch it.

If a personal token is genuinely unavoidable, use a project access token scoped to one project with read_repository only.

Go modules

variables:
  GOPRIVATE: gitlab.com/acme/*
  GOFLAGS: -mod=mod
before_script:
  - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"

GOPRIVATE stops Go trying the public module proxy and checksum database for those paths, which otherwise fails with a 404 that has nothing to do with authentication and sends you looking in the wrong place.

Making the failure clearer

By default Git hangs or errors obscurely when it wants a prompt. Make it explicit:

variables:
  GIT_TERMINAL_PROMPT: "0"

It fails immediately with terminal prompts disabled, which is a much clearer message than No such device or address and avoids a job that sits waiting until it times out.

A checklist

  1. Identify which repository is being fetched. It is rarely the main checkout.
  2. Submodules → GIT_SUBMODULE_STRATEGY: recursive plus GIT_SUBMODULE_FORCE_HTTPS.
  3. Anything else → git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf.
  4. The username is the literal string gitlab-ci-token.
  5. HTTP Basic: Access denied → allowlist the source project in the target's job token permissions.
  6. Cross-host or long-lived → a deploy key in a File-type CI variable.
  7. Never embed a personal token in a clone URL.
  8. Go → set GOPRIVATE as well as the insteadOf rewrite.

Frequently Asked Questions

Why does Git ask for a username in CI when it never does locally?

Locally a credential helper supplies the credential, or Git prompts you on the terminal. A CI job has neither: no helper is configured for the repository being fetched, and there is no terminal to prompt on, so Git fails with No such device or address or, on newer versions, terminal prompts disabled. The message describes the missing prompt, but the underlying problem is always the missing credential. Setting GIT_TERMINAL_PROMPT=0 makes the failure immediate and clearer.

What is CI_JOB_TOKEN and how do I use it?

It is a short-lived token GitLab injects into every job, valid only for that job's duration, used with the literal username gitlab-ci-token. The cleanest way to apply it is a git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/" rewrite, which makes it apply to every matching URL including ones inside .gitmodules and inside package managers you do not control. Since GitLab 16 the target project must also allowlist the source project under job token permissions.

Why does my submodule fail when the main repository clones fine?

Because the runner configures credentials for the project being built and nothing else. A submodule URL in .gitmodules is fetched as a separate operation with no credentials attached, so a private submodule is refused. Either set GIT_SUBMODULE_STRATEGY: recursive so the runner's own checkout fetches them with credentials already in place, or apply an insteadOf rewrite before the submodule update runs. Adding GIT_SUBMODULE_FORCE_HTTPS also converts git@ URLs so the token applies.

Should I use a personal access token in CI?

No. A personal token carries your full access, so any job using it can reach every project your account can, and it usually outlives the reason it was created. It also tends to end up embedded in a clone URL, which is written into .git/config and often into job logs where masking does not reliably catch it. Use CI_JOB_TOKEN for same-instance access, a project access token scoped to one project with read_repository where that is not enough, or a deploy key.

What is the difference between this error and "HTTP Basic: Access denied"?

The first means no credential was supplied at all, so Git wanted to prompt and could not. The second means a credential was supplied and rejected, which in GitLab 16 and later most often means the job token is not allowlisted for the target project rather than that it is wrong. They are consecutive stages of the same problem, and moving from the first message to the second is actually progress: you have fixed the credential plumbing and now need to grant access.

Reference and practice

Learn the underlying concept

Other CI/CD errors