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 clonein 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
- Identify which repository is being fetched. It is rarely the main checkout.
- Submodules →
GIT_SUBMODULE_STRATEGY: recursiveplusGIT_SUBMODULE_FORCE_HTTPS. - Anything else →
git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf. - The username is the literal string
gitlab-ci-token. HTTP Basic: Access denied→ allowlist the source project in the target's job token permissions.- Cross-host or long-lived → a deploy key in a File-type CI variable.
- Never embed a personal token in a clone URL.
- Go → set
GOPRIVATEas 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.