Saving to GitHub needs an OAuth App (not a GitHub App) so users can log in without pasting tokens. Git operations themselves run server-side in the session's Durable Object with just-git (pure TypeScript, Smart HTTP to github.com). Tokens stay in DO storage; the browser only ever sees the login redirect.
GitHub Settings → Developer settings → OAuth Apps → New. Do not use
GitHub Apps: its user tokens (ghu_…) pass every API check yet GitHub
refuses each git push with 403 "Permission denied" — the exact symptom is
"clone works, push always 403s, token scopes unknown". OAuth-App tokens
(gho_…) push fine. If the panel ever reports token type: github-app,
delete that credential path and start over here.
GitHub Settings → Developer settings → OAuth Apps → New:
- Homepage URL:
https://<your-worker>.workers.dev - Authorization callback URL:
https://<your-worker>.workers.dev/api/github/callback
For local dev (wrangler dev), create a second app with callback
http://localhost:8787/api/github/callback — GitHub requires an exact match.
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRETLocally, the same keys work in .dev.vars (gitignored).
- User opens the GitHub tab → Log in with GitHub.
- Worker redirects to github.com with
state= session id. - GitHub redirects to
/api/github/callback?code&state; the Worker exchanges the code for a token and stores it (plus the user) in that session's DO, then sets the session cookie tostateso the browser lands back on the same session. - Open repo (
owner/repo) clones via just-git into the session's files; Create + push creates the repo via the GitHub API then pushes the current files as the first commit; Commit + push / Pull / Status / Log / Run git work from the panel, andgit_status,git_log,gittools expose the same to the agent.
- Working tree is text-only (
FileMapispath → string). Binary, oversized (>1 MB), and unsafe-path files are skipped on clone/pull and reported in the panel — but commit, pull, status, and Run git re-materialize them from the git object store first, so status stays clean and commits leave them untouched. They just are not editable here. File deletion goes throughgit rmin the panel's Run git box. - The git database (
/repo/.git) is snapshotted to DO SQLite after every op; very large histories will be slow. Shallow or small repos work best. - History rewrites are refused server-side (
reset,rebase, force pushes). - OAuth scope is
repo workflow(workflowcovers pushes touching.github/workflows/*, which GitHub 403s underrepoalone). Revoke from GitHub Settings to disconnect; Log out clears the session copy including the repo link and stored history. - If you logged in before the
workflowscope was added, Log out/in again to grant it. The GitHub tab shows the token's scopes next to the login, so a stale token is visible. Pushes that touch.github/workflows/*with aworkflow-less token are refused before committing, with the re-login fix — nothing strands locally. - Reopening the same repo (including after a logout/login) keeps local edits as uncommitted changes instead of cloning over them; opening a different repo still clones clean. The panel reports how many files were kept.
- Clone reads public repos without write access, so a push can still 403 after a
good clone: the push error names the fix (collaborator access, fork, or reconnect).
When the REST pre-check passes but git still refuses, the error includes push
diagnostics (token scopes, whether the push touches
.github/workflows/*, and a read-only probe of the git push endpoint) pinpointing scope vs. repo-rule causes.