●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
A scheduled task that updates one of my website repositories stopped on its very first clone one day. The log held only these two lines.
fatal: could not read Username for 'https://github.com': No such device or addressfatal: expected flush after ref listing
Another task that ran earlier the same morning had pushed without trouble. What changed in between, I still can't say. What I could do was pin down why this one stopped, and this is a record of how, and of where I settled on handing git its credentials.
The short version: writing only the token in the URL (https://TOKEN@github.com/...) failed in this environment, while adding a user name (https://x-access-token:TOKEN@github.com/...) worked. And the form that works has its own problem: the token ends up in the repository's config file.
What was actually happening
"Cannot read Username" is an odd complaint when the URL already holds a token. The message didn't seem to fit, so I looked at what git actually sent.
# Keep only the interesting lines (git itself redacts the token in this trace)GIT_TRACE_CURL=1 GIT_TRACE_CURL_NO_DATA=1 \ git ls-remote "https://TOKEN@github.com/OWNER/REPO.git" HEAD 2>&1 \ | grep -E "Send header: (GET|authorization)|Recv header: HTTP|redirecting|fatal"
With the token-only form, the exchange went like this.
=> GET /OWNER/REPO.git/info/refs?service=git-upload-pack<= HTTP/2 401=> GET /OWNER/REPO.git/info/refs?service=git-upload-pack=> authorization: Basic <redacted><= HTTP/2 200warning: redirecting to https://github.com/OWNER/REPO.git/<= HTTP/2 401fatal: could not read Username for 'https://github.com'
After the first 401, the retry with credentials succeeded and returned 200. Right after that git printed "redirecting", and the third request carried no authorization header. That request got a 401, git tried to ask for a Username, and with no terminal available it gave up.
With the user-name form, the same warning appeared, but the third request did carry the header and returned 200.
URL form
Request sequence
Result
Token only
401 → 200 → (warning) → 401 without auth
Failed
User:token
401 → 200 → (warning) → 200 with auth
Worked
I haven't worked out why only the first form drops its credentials, or where the "redirecting" warning comes from. From the URL alone I couldn't even tell whether GitHub or something in front of it produced the redirect. What I could confirm is that the way the URL is written decides whether authentication survives the same warning. The git version was 2.34.1.
Six ways of handing over a token, side by side
Finding a form that works would have been a fine place to stop, but then I started wondering where the token ends up. The procedure I'd been following includes git remote set-url origin https://TOKEN@github.com/..., which writes the token into .git/config. So I tried each method and looked at two things: the config file, and the process list while git was running. I used a dummy token for that.
Method
Worked?
Stays in config?
Visible in process arguments?
Token only in the URL
Failed
Yes, if set as a remote (same mechanism as user:token)
Yes, if the URL is passed as an argument
User:token in the URL
Worked
Yes (and git remote -v prints it as is)
Yes
http.extraHeader
Worked
No
Yes (the Base64 undoes in one line)
GIT_ASKPASS (token in an environment variable)
Worked
No
No (it is in the environment)
GIT_ASKPASS (token read from a file)
Worked
No
No (not in the environment either)
credential.helper store
Only checked that it saves
Yes, in a plain file (mode 600)
Not measured
What surprised me was that the token-only form, the one I see most often, was the only one that failed. And the one method that both works and leaves nothing behind is the fifth row, which is also the most tedious to write.
The Base64 in extraHeader is readable. When I decoded the string I could see in the process list, it came back as x-access-token: followed by the token, unchanged. That is a rewording, not encryption, and I'd treat it as such.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦When git stops on authentication inside a scheduled task, you can read three lines of the HTTP trace and tell whether the URL form is the cause
✦You can drop a ten-line wrapper into your own setup that calls git without leaving the token in a config file or in process arguments
✦You can find, with a read-only script, which of your local repositories still carry a token inside the remote URL
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Step 1: call git without leaving the token in arguments or config
I chose GIT_ASKPASS with the token read from a file. The wrapper writes the value to a temporary file and always removes it on exit.
#!/bin/bash# ghgit.sh — a thin wrapper that calls git without putting the token in a URL or an argument# Usage: ghgit.sh "label" <git subcommand and arguments...>set -euo pipefailLABEL=${1:?usage: ghgit.sh "label" <git arguments...>}; shiftTOKENS=${TOKENS_FILE:?set TOKENS_FILE to the path of your token ledger}tok=$(mktemp "$HOME/.ghtok.XXXXXX"); chmod 600 "$tok"ask=$(mktemp "$HOME/.ghask.XXXXXX")trap 'rm -f "$tok" "$ask"' EXIT# The ledger format is "label line, then the token on the next line". Never echo the value.{ grep -A1 -F -- "$LABEL" "$TOKENS" || true; } | tail -n 1 | tr -d "[:space:]" > "$tok"[ -s "$tok" ] || { echo "ghgit: no token found for label '$LABEL'" >&2; exit 2; }cat > "$ask" <<'ASK'#!/bin/shcase "$1" in Username*) echo x-access-token ;; *) cat "$GHGIT_TOKEN_FILE" ;;esacASKchmod 700 "$ask"GHGIT_TOKEN_FILE="$tok" GIT_ASKPASS="$ask" GIT_TERMINAL_PROMPT=0 git "$@"
To use it, put ghgit.sh "label" in front of git. The URLs stay free of credentials.
export TOKENS_FILE="$HOME/secrets/tokens.txt" # adjust to wherever yours lives./ghgit.sh "My Repo" clone --depth 1 --branch main https://github.com/OWNER/REPO.git work./ghgit.sh "My Repo" -C work pull --rebase origin main./ghgit.sh "My Repo" -C work push origin main
Here is why each piece is written the way it is.
GIT_ASKPASS asks for the user name and the password separately. Branching on whether the prompt starts with Username keeps the script short. For a GitHub token, x-access-token worked as the user name
The token never goes into an environment variable; only the file's location does. A value in the environment can be read by other processes of the same user. A file can be limited with mktemp and chmod 600, and trap removes it on exit
GIT_TERMINAL_PROMPT=0 is there on purpose. In an unattended run nobody can answer a prompt, and I would rather git fail immediately than wait
The remote of the working repository stays a plain URL. If someone opens the config file, there is no token to find
What I checked: clone, pull and push --dry-run all went through (the remote of the cloned repository remained a credential-free URL). While a run was in flight, ps showed only git ls-remote https://..., and zero lines containing the token. The environment of the running git process did not contain the token string either.
Step 2: do not let a missing label end the script silently
The first version of this wrapper ended in silence. Call it with a wrong label and it printed nothing and returned exit code 1.
With set -e and pipefail, the moment grep found nothing (exit code 1) the whole pipeline failed and the script exited. The message I wrote for exactly this case never got a chance to run. In a scheduled task that leaves a log with a failure and no explanation, which is the form I find hardest to read afterwards. Wrapping only the grep in braces with || true, and moving the judgment to the empty-file check on the next line, produced the message and exit code 2.
Treat "not found" as an ordinary outcome first, then stop with your own words. That order keeps the cause readable in the log. I wrote about the same idea of checking results before exiting in the end-of-run assertion article.
Step 3: count the repositories that still have a token in the URL
Even after switching to the new form, the .git/config of every repository you created earlier still holds the old token. Before changing anything, count them with a read-only script. It never prints the token itself.
#!/bin/bash# Count git repositories directly under a folder whose remote URL has credentials embedded.# Read-only. The contents of the URL (the token itself) are never printed.root=${1:?usage: scan_remote_creds.sh <folder that holds your repositories>}total=0; embedded=0for d in "$root"/*/; do [ -d "$d/.git" ] || continue total=$((total + 1)) # Look at every remote, not only origin while read -r name; do url=$(git -C "$d" remote get-url "$name" 2>/dev/null) || continue case "$url" in *://*@*) embedded=$((embedded + 1)); echo "embedded: ${d%/} ($name)";; esac done < <(git -C "$d" remote)doneecho "${embedded} remote(s) with credentials across ${total} repositories"
I ran it on two dummy repositories, one with a token and one without, and got this.
embedded: /.../scan/r1 (origin)1 remote(s) with credentials across 2 repositories
Anything it finds can go back to a credential-free URL with git remote set-url origin https://github.com/OWNER/REPO.git. After that, I'd also regenerate any token that was ever written into a URL, because config files can be copied by folder-sync services and backups.
Pitfalls
The "redirecting" warning appeared with every form. The warning alone is not a sign of failure. Look at the trace to see whether authentication survived or dropped after it
I did not try other git versions. I used 2.34.1. Newer versions may treat credentials embedded in a URL differently
Environments with a credential helper may behave differently. There was no helper here, so GIT_ASKPASS was used. I haven't tried setups that already have one, such as a macOS keychain integration
The wrapper depends on the ledger format. It assumes the token sits on the line after the label, so a label on the last line would pull in the label line itself. If a label matches several lines, the line after the last match is used
trap does not run on a forced kill. While testing, I stopped a running git with pkill, my working shell went down with it, and four .ghtok.* and .ghask.* files stayed in my home folder. They held the real token. I deleted them right away, but the warning about forced kills turned out to be a personal one. A line at startup that removes stale .ghtok.* files would make it safer still
Visible arguments only matter on a shared machine. My environment has a single user, so even extraHeader would have been low-risk. I still prefer to write the procedure in the form that leaves the least behind
Where I drew the line
Working is not the same as adopting it. In this case, getting git to pass was only the first half; the second half was making it leave nothing behind.
As an indie developer who runs websites alone, I'd suggest starting with the Step 3 script, run read-only on the folder where your repositories live. Zero is the best result. If it finds even one, decide whether to regenerate that token before you change the URL back.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.