CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-06-01Intermediate

Fixing 403 Write Access to Repository Not Granted When Pushing from Claude Code

When Claude Code automation pushes to GitHub and hits a 403 'Write access to repository not granted', the cause usually lies in how fine-grained PATs differ from classic ones. Here's how to diagnose repository access, Contents permissions, and org approval.

troubleshooting87claude-code129github3git16fine-grained-pat403

The other day I was running a setup that updates several repositories automatically from Claude Code, and one repository stalled right at git clone. The message that came back was:

remote: Write access to repository not granted.
fatal: unable to access 'https://github.com/youruser/yourrepo.git/': The requested URL returned error: 403

The token had been regenerated just the day before, and every other repository was pushing fine through the exact same setup. Yet this one repository returned a 403. The cause turned out to be a misunderstanding of how GitHub's fine-grained personal access tokens handle permissions. Anyone automating repository operations from Claude Code can fall into the same trap, so let me walk through how to tell the errors apart and the order in which to check things.

First, separate 401 from 403 — they are not the same problem

The first thing to do is determine whether the error is an authentication failure or an authorization failure. Confuse the two and you'll regenerate tokens endlessly without fixing anything.

  • fatal: Authentication failed / 401 — the token isn't being accepted by GitHub at all. Usually a mistyped string, an expired token, or a stray newline introduced when copying.
  • Write access to repository not granted / Resource not accessible by personal access token / 403 — the token is valid, but it has no write (or even read) permission tied to that specific repository.

What I hit was the latter, a 403. The token was alive — git ls-remote even succeeded in some cases — yet only the push was rejected. That pattern points squarely at a permission-scope problem.

As a first step, confirm only whether the token authenticates at all.

# Is authentication succeeding? (distinguishes 401 from 403)
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer ${GITHUB_TOKEN}" \
  https://api.github.com/user
# A 200 means authentication works — the token itself is valid.

If this returns 200 but the push still returns 403, the problem isn't token validity. It's access to that particular repository.

The root cause — classic and fine-grained PATs use different permission models

This is the heart of what tripped me up. GitHub offers two kinds of PAT, and they think about permissions in completely different ways.

Classic PATs (starting with ghp_) grant read and write across every repository the account can reach once you tick the repo scope. There's no notion of choosing repositories individually, so a token you created once keeps working for newly added repositories too.

Fine-grained PATs (starting with github_pat_) require you to explicitly choose which repositories and which operations the token may perform, at creation time. If you forget to select a target repository — or a repository created later was never added to the selection — the token stays valid but returns 403 for that repository alone.

My case was exactly this: when I regenerated the token, I had missed selecting one repository in the list. The reason it behaved differently from another token where git ls-remote worked was simply that the other token was a classic PAT covering all repositories.

Checklist when using a fine-grained PAT

When a fine-grained PAT returns 403, check these three things, top to bottom.

1. Is the target repository included in Repository access?

In GitHub, go to Settings → Developer settings → Personal access tokens → Fine-grained tokens, open the token, and check Repository access. If you chose Only select repositories, the repository you want to operate on must be in the list. If it isn't, add it. For an organization's repositories, even All repositories requires that the organization itself appears as an option.

2. Is the Contents permission set to Read and write?

Fine-grained PATs split permissions per operation. git clone / git pull need Contents Read, while git push needs Contents Read and write. If Repository permissions → Contents is left at Read-only, you can clone but the push returns 403. That's the real explanation for the "I can read it but I can't write it" state.

# Check directly whether the token has rights to the target repository
curl -s -H "Authorization: Bearer ${GITHUB_TOKEN}" \
  https://api.github.com/repos/youruser/yourrepo \
  | grep -E '"(full_name|permissions)"' -A4
# Look at "permissions": { "push": true, ... }
# "push": false means Contents is Read-only.

If you see "push": false, change Contents to Read and write in the token settings. Note that fine-grained PATs can take a few dozen seconds to reflect a permission edit.

3. Is it waiting on organization approval?

If the repository is owned by an organization, a fine-grained PAT won't work until the organization approves it. If the token shows as Pending in the token list, an org administrator needs to approve it. For repositories under a personal account, this step doesn't apply.

Practical ways to keep automation running

As an indie developer who has shipped apps since 2014 (Dolice, with tens of millions of downloads across them), I update several repositories through one shared setup — which leaves a standing risk of forgetting to update the fine-grained PAT's selection whenever I add a repository. So in practice I settled on a split: assign a classic PAT (with the repo scope) to automation that spans many repositories, and reserve fine-grained PATs for cases where I want to expose just one repository externally. Fine-grained errs on the safe side, which is exactly why a missed selection surfaces as a silent 403.

When you need a push to go through urgently, the fastest path is to temporarily swap in another classic PAT on the same account that already covers all repositories. Then, calmly fix the Repository access and Contents permission on the fine-grained PAT you actually intended to use.

# Push via an environment variable rather than embedding the token in the URL
# (keeps the token out of shell history and process listings)
git remote set-url origin "https://github.com/youruser/yourrepo.git"
git -c credential.helper='!f() { echo "username=x-access-token"; echo "password=${GITHUB_TOKEN}"; }; f' \
  push origin main

Quick triage table

When you run into a 403, working through these in order gets you to the cause fastest.

  1. Does api.github.com/user return 200? → If no, it's a 401 (token string or expiry).
  2. Is permissions.push true on repos/.../? → If false, Contents permission is insufficient.
  3. For fine-grained tokens, is the repository in the selection? → If missing, add it.
  4. If org-owned, is approval Pending? → If so, ask an admin to approve.

Before you regenerate a token, try these four checks first. More often than not the token itself is fine, and a single permission selection is simply missing. I hope this saves a little time for anyone stuck at the same spot.

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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-10
When git add -A Sweeps Up .bak Backups — The Quiet Trap of In-Place Fix Scripts
How .bak backups left by sed -i and homegrown --fix scripts get silently swept into production by git add -A, why it is so easy to miss, and how scoped adds and .gitignore close the gap for good.
Claude Code2026-06-03
When git push Says Success but Nothing Lands — the Silent commit Failure in Claude Code
git push prints Everything up-to-date and exits zero, yet your changes never reach the remote. Here is why commit silently fails on a fresh sandbox clone, and how to verify a real push with SHA comparison.
Claude Code2026-05-12
Accidentally Committed API Keys to Git — Emergency Response with Claude Code
Step-by-step emergency guide for when you accidentally commit .env API keys to git. Covers immediate key rotation, removing secrets from git history with Claude Code, and preventing it from happening again.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →