Clone a private repository into a sandbox while the access token stays in your Worker. If the clone URL contains the token, every command in the sandbox can read it. Instead, the Durable Object intercepts HTTPS requests from Git to github.com. An entrypoint in your Worker receives each request, checks it, and adds the token.
- A Worker with a Durable Object that starts a container with the Durable Object scheduling policy. To create one, refer to Run a Linux command.
- A GitHub access token with read access to the contents of the repository.
You must have Docker running locally when you run wrangler deploy. For most people, the best way to install Docker is to follow the docs for installing Docker Desktop ↗︎. Other tools like Colima ↗︎ may also work.
You can check that Docker is running properly by running the docker info command in your terminal. If Docker is running, the command will succeed. If Docker is not running,
the docker info command will hang or return an error including the message "Cannot connect to the Docker daemon".
-
Create a
Dockerfilein your project root that installs Git:Dockerfiledockerfile FROM node:24-trixie-slim RUN apt-get update \ && apt-get install --yes --no-install-recommends ca-certificates git \ && rm -rf /var/lib/apt/lists/* CMD ["sleep", "infinity"] -
In
wrangler.jsonc, name the repository, declare the token as a secret, and build theDockerfileas a named image. Replace<OWNER>/<REPOSITORY>with your repository:{ "vars": { "REPOSITORY": "<OWNER>/<REPOSITORY>", }, "secrets": { "required": ["GITHUB_TOKEN"], }, "containers": [ { "class_name": "MyContainer", "scheduling_policy": "durable_object", "images": { "git": { "dockerfile": "./Dockerfile", }, }, }, ], }[vars] REPOSITORY = "<OWNER>/<REPOSITORY>" [secrets] required = [ "GITHUB_TOKEN" ] [[containers]] class_name = "MyContainer" scheduling_policy = "durable_object" [containers.images.git] dockerfile = "./Dockerfile"npx wrangler typesyarn wrangler typespnpm wrangler types -
Store the token when Wrangler prompts for it:
npx wrangler secret put GITHUB_TOKENyarn wrangler secret put GITHUB_TOKENpnpm wrangler secret put GITHUB_TOKEN -
Add an entrypoint to your Worker that accepts only the requests that fetch the repository, and adds the token:
src/index.jsjs import { WorkerEntrypoint } from "cloudflare:workers"; export class GitGateway extends WorkerEntrypoint { async fetch(request) { const url = new URL(request.url); const repository = `/${this.env.REPOSITORY}.git`; const fetchesRepository = (request.method === "GET" && url.pathname === `${repository}/info/refs` && url.search === "?service=git-upload-pack") || (request.method === "POST" && url.pathname === `${repository}/git-upload-pack`); if (url.hostname !== "github.com" || !fetchesRepository) { return new Response("Forbidden", { status: 403 }); } const headers = new Headers(request.headers); const credentials = btoa(`x-access-token:${this.env.GITHUB_TOKEN}`); headers.set("Authorization", `Basic ${credentials}`); return fetch(new Request(request, { headers })); } }src/index.tsts import { WorkerEntrypoint } from "cloudflare:workers"; export class GitGateway extends WorkerEntrypoint<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const repository = `/${this.env.REPOSITORY}.git`; const fetchesRepository = (request.method === "GET" && url.pathname === `${repository}/info/refs` && url.search === "?service=git-upload-pack") || (request.method === "POST" && url.pathname === `${repository}/git-upload-pack`); if (url.hostname !== "github.com" || !fetchesRepository) { return new Response("Forbidden", { status: 403 }); } const headers = new Headers(request.headers); const credentials = btoa(`x-access-token:${this.env.GITHUB_TOKEN}`); headers.set("Authorization", `Basic ${credentials}`); return fetch(new Request(request, { headers })); } }git-upload-packserves clones and fetches. The entrypoint allows it for one repository and rejects every other request. Code in the sandbox cannot push throughgit-receive-packor read other repositories, even when the token allows it. -
Add a method to your Durable Object that intercepts
github.comand clones the repository:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async clone(): Promise<{ exitCode: number; stderr: string }> { const container = this.ctx.container; if (!container) { throw new Error("The container binding is not configured"); } await container.interceptOutboundHttps( "github.com", this.ctx.exports.GitGateway, ); if (!container.running) { container.start({ image: container.images.git, // The container can reach only the hostnames you intercept enableInternet: false, }); } const process = await container.exec( [ "git", "clone", "--depth=1", `https://github.com/${this.env.REPOSITORY}.git`, "/workspace", ], { env: { // The intercept terminates TLS with a certificate that the container // CA certificate signs. Git reads the CA certificate from this path GIT_SSL_CAINFO: "/etc/cloudflare/certs/cloudflare-containers-ca.crt", }, }, ); const output = await process.output(); return { exitCode: output.exitCode, // Git prints its progress to standard error stderr: new TextDecoder().decode(output.stderr), }; } } -
Add a route to your Worker that clones the repository:
src/index.tsts if (url.pathname === "/clone" && request.method === "POST") { const sandbox = env.MY_CONTAINER.getByName("sandbox"); const { exitCode, stderr } = await sandbox.clone(); return new Response(stderr, { status: exitCode === 0 ? 200 : 500 }); }When Git fails, the route responds with
500and the error from Git. While the sandbox keeps running, a second request fails this way, because/workspacealready holds the clone.Authenticate callers first, so other people cannot read the repository through the sandbox. For more information, refer to Sandbox security.
-
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
Send a
POSTrequest to/cloneon theworkers.devURL that Wrangler prints:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/clone --request POSTCloning into '/workspace'...The repository is in
/workspace, andgit remote get-url originprints the URL without a token. Agit pushfrom the sandbox fails with403.
- Run tests from a Git repository: install and test a cloned repository.
- Call an authenticated API from a sandbox: the same pattern for an API.
interceptOutboundHttps