# Getting Claude Code's commits out of the container with a fetch-only remote

In part 2 of this series I set up Claude Code for a brownfield project: an existing codebase with years of history behind it, as opposed to a new project started from scratch. The real repo has old credentials somewhere in its history, so Claude doesn't get the real repo. It gets a copy: no `.git`, secrets scrubbed, `gitleaks` run over it, then a fresh `git init`. The container only has that copy mounted. It can't see the real repo at all.

That part works. The annoying part was getting Claude's work back out. I always had two projects open: my original repo, and the copy Claude works in. Every time Claude finished something, I had to switch to the copy and push its branch across into the original, and only then could I cherry-pick in my own project.

Now I only open the original project. I never have to open the copy Claude works in.

## Why not let Claude push to the real repo?

The obvious alternative is giving the container access to the real remote, so Claude can push its branch there and it simply shows up next to mine. For this repo that doesn't work: access that allows pushing also allows fetching. One `git fetch` would pull the real history, credentials included, straight into the container. That undoes the whole reason for the copy.

The same goes for a shared bare repo as a "bridge" between the two: if it's created from the real repo, it carries the real history too.

## The right fix: reverse the direction

My machine can already see both folders. Claude's copy is just a bind-mounted folder on my own disk. So instead of Claude pushing into the real repo, the real repo fetches from Claude's copy.

A repo can have as many remotes as you like. `origin` is just the default name for the one you cloned from. So in the real repo, on the host, I add a second one:

```bash
git remote add claude /path/to/claude-copy
git remote set-url --push claude no-push-allowed
```

The second line is the important one, and the easy one to forget: `git remote add` sets both the fetch and the push URL to the copy. The second line keeps fetching from the copy working, but points the push URL at something that doesn't exist. An accidental `git push claude` then fails instead of copying the real history, secrets and all, into the folder Claude can see.

On Windows, use the path the way Windows git sees it, with forward slashes, for example `C:/Workspace/Claude/my-repo`, not the `/workspace` path from inside the container.

```bash
git remote -v
# origin  https://example.com/me/real-repo.git (fetch)
# origin  https://example.com/me/real-repo.git (push)
# claude  C:/Workspace/Claude/my-repo (fetch)
# claude  no-push-allowed (push)
```

Each remote keeps its branches under its own name (`origin/master` next to `claude/some-branch`), so nothing collides. My existing branches keep tracking `origin`, and a plain `git push` or `git pull` still goes where it always did.

## The workflow now

1. Claude works on a branch in its copy and commits. It doesn't push anything, and it has nothing to push to.
2. In my own real repo, I fetch its commits and see what's new:

```bash
git fetch claude
git log --oneline master..claude/<branch>
```

3. I branch off master and cherry-pick the commits I want to keep:

```bash
git checkout -b my-feature master
git cherry-pick <commit1> <commit2>
```

4. That branch is the one that becomes the PR.

The copy and the real repo have unrelated histories, since the copy started from its own `git init`. Cherry-picking doesn't care: it applies each commit's changes, not its ancestry. The one thing to skip is the copy's first commit, the one that adds the whole codebase.

Compared to part 2, the temporary remote and the manual `git push origin other-repo/<branch>:<new-branch>` step are gone. The remote stays configured, so from then on it's only `fetch` and `cherry-pick`.

## Gotcha: Visual Studio doesn't show the new remote's branch

After adding the remote and fetching, `git branch -r` listed `claude/claude-work` just fine, but Visual Studio's Git Repository window didn't show it. Git had the branch; the window just hadn't picked it up.

The first time, fetching from Visual Studio's own interface doesn't pick up the new remote's branch. What works: run `git fetch claude` as a command in Visual Studio's terminal (**View > Terminal**), then close the Git Repository window and open it again. That's only needed once: as soon as the branch shows up under the remotes, fetching it from Visual Studio works too. After that, the new remote showed up with its branch, and from its history you can right-click a commit and choose **Cherry-Pick**.

You can also add the remote in Visual Studio, under **Git > Settings > Git Repository Settings > Remotes**. That dialog asks for both a fetch and a push URL, so there's no way to add a fetch-only remote there. Fill in the copy's path for both, then make the push side unusable with the same command as above:

```bash
git remote set-url --push claude no-push-allowed
```

`git remote -v` should then show `no-push-allowed` as the push URL.

## Why cherry-pick

Cherry-picking makes me choose every commit that goes into the real repo, instead of merging whatever ended up on Claude's branch in one go. If an experiment or a half-finished detour went in along the way, it simply doesn't get picked.

It also keeps the review where I already work: in my own solution, with my own tools.

## One-way by design

History only ever moves one way: from the copy into the real repo, never back. The container sees one folder with a clean, secret-free history, and that's still all it sees. There's no token and no extra container, and nothing about the container setup from part 1 had to change.

## Treat the copy as untrusted

There's one more direction to think about. Claude can write anything in the copy, including its `.git` folder, so from my own machine I treat the copy as untrusted.

That matters because a repo's own `.git/config` can make git run commands. A setting like `core.fsmonitor` names a program that git starts whenever you run `git status` in that repo, and hooks in `.git/hooks` run on commits and checkouts. None of that is copied when you clone or fetch, but it does run when you work *in* that repo.

So my rule: on Windows, I never run git in the copy and never open it in Visual Studio, which runs git in the background as soon as it opens a repo. Everything that happens inside the copy, Claude does inside the container, where it can't reach anything else anyway. From my side, I only fetch from it and write patch files next to it.

Fetching is the one exception, and it's a considered one. The server side of a fetch (`git upload-pack`) is designed to ignore the dangerous settings and hooks of the repo it reads from, which is why git's own documentation calls it safe to fetch from an untrusted repo. The same documentation also notes that its attack surface is large, so it's not zero risk. Keeping git on Windows up to date is part of the deal.

Two related things:

- If git ever refuses to work with a repo because of `safe.directory` ("detected dubious ownership"), don't "fix" it with `git config --global --add safe.directory '*'`. That switches the ownership check off for every repo on the machine, and that check exists for exactly this kind of situation. If you must, add only the one path you trust.
- Don't use `git push --mirror` on the real repo. A mirror push sends every ref, including `refs/remotes/claude/*`, so Claude's branches would end up on `origin` without ever going through a review.

## Keeping the copy up to date

One-way has a catch. When I change something myself in the real repo, Claude never sees it. The copy falls behind, Claude keeps building on outdated code, and cherry-picking its commits onto my newer master can conflict wherever we both touched the same lines.

Fetching from the real repo in the copy would fix that, and bring the whole history back with it. So my own commits go over as patches instead. A patch is a plain text file with one commit's message, author and diff, and nothing of the history before it.

I don't even have to open the copy for this. From the real repo, I write the patch straight into a folder in the mounted workspace, next to the copy:

```bash
# in the real repo: export one commit as a patch file
git format-patch -1 <commit> -o /path/to/mounted-workspace/claude-sync
```

Then I tell Claude there's a patch waiting, and it applies it in the copy with `git am`, as a real commit, and continues from there. No push, no fetch between the two repos: it's just a file in a folder both of us can reach. If a patch doesn't apply cleanly, for example because it touches a file I scrubbed in the copy, Claude resolves that on its side too.

To make exporting a single command, a git alias in the real repo helps:

```bash
git config alias.to-claude "format-patch -o /path/to/mounted-workspace/claude-sync"

git to-claude -1 <commit>           # one commit
git to-claude <since-commit>..master  # everything after a given commit
```

So in both directions I only work from my own project: `git fetch claude` and cherry-pick to get Claude's work, `git to-claude` to send mine.

In practice I rarely need the patches, though. When I want a change or a fix, I don't make it in the real repo myself: I ask Claude to make it in the copy, and then cherry-pick its commit like any other. The change starts out in the copy, so the copy doesn't fall behind and there's nothing to send over. Patches are still there for the changes that do start on my side.

### Gotcha: line endings

My first patch didn't apply. The file was identical in both repos, but git couldn't match the lines.

The cause: my Windows clone uses `core.autocrlf=true`, so the files on disk have CRLF line endings, but git stores them with LF. The copy was made from the files on disk, and `git init` inside the Linux container stored them as they were, with CRLF. Same text, stored differently, and patches are made from what git stores.

Since I never open the copy on Windows anymore, the simplest fix was making the copy all LF, once:

```bash
# Claude ran this inside the container, in the copy:
# commit or stash everything first, the last step rewrites the files on disk
echo "* text=auto eol=lf" >> .git/info/attributes
git add --renormalize .
git commit -m "Normalize line endings to LF"
git rm --cached -r -q .
git reset --hard
```

`.git/info/attributes` works like a `.gitattributes` file, but it stays local, so it never ends up in a commit I cherry-pick. `git ls-files --eol` should then show `i/lf w/lf`. After that, patches apply with a plain `git am`, and Claude's commits cherry-pick cleanly into my Windows clone, where they show up with CRLF like every other file. Just skip the normalize commit when cherry-picking, like the copy's first commit.

### Two more things to watch

- The patches are the one thing that travels from the real repo into the copy. They only contain my new changes, so they're safe as long as I didn't commit a secret in those. A quick `gitleaks dir /path/to/mounted-workspace/claude-sync` before handing them over is cheap insurance.
- Don't sync by copying files over instead. The current files in the real repo can still contain the secrets I scrubbed from the copy. Patches only carry what changed.

---

*Co-authored with Claude.*
