> ## Documentation Index
> Fetch the complete documentation index at: https://code.storage/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Export Your Repositories

> Receive a copy of your Git repositories and LFS objects in an Amazon S3 bucket that you own, then verify and restore them on another Git host.

Your code is yours, and you can take a full copy of it whenever you need to. Email
[support@pierre.co](mailto:support@pierre.co) and we'll export your repositories into an S3 bucket
in your own AWS account. We'll agree a time with you, then walk through the steps on this page
together.

You receive one archive for each repository, with all refs and history, plus every Git LFS object.
You own the copied objects, and you can restore them on any Git host.

An export copies data and deletes nothing. Your repositories stay in Code Storage until you confirm
the export and ask for deletion.

## How it works

```text theme={"theme":{"light":"github-light","dark":"min-dark"}}
1. You email support@pierre.co, create a bucket, and send us the destination folder.
2. We send you a bucket policy. You apply it.
3. You stop using your repositories. We run the export into your folder.
4. You download, verify, and restore the export.
5. You remove the bucket policy.
```

The bucket policy lets only the export job for your organization write to your folder. It grants no
delete permission and no access outside the folder.

## Placeholders

The commands and policies on this page use the placeholders below. Replace each placeholder before
you run a command.

| Placeholder | Replace with |
| - | - |
| `YOUR_BUCKET` | The name of your S3 bucket, for example `acme-code-storage-export` |
| `YOUR_FOLDER` | A folder name in the bucket, for example `code-storage-2026-11` |
| `YOUR_REGION` | The AWS region of the bucket, for example `us-east-2` |
| `YOUR_KMS_KEY_ARN` | The ARN of your KMS key, only if you use SSE-KMS |
| `REPO_ID` | A repository ID from the export catalog |
| `NEW_GIT_URL` | The URL of an empty repository on the Git host where you restore a repository |
| `OWNER/REPO` | The restored repository on GitHub, only if you restore to GitHub |
| `DEFAULT_BRANCH` | The `default_branch` value from the export catalog |

Values that start with `CODE_STORAGE_` come from us. The policies that we send you have them filled
in. You do not need to change them.

## Before you start

You need:

* An AWS account and the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/) version 2,
  signed in with permission to create buckets and set bucket policies.
* `jq`, `zstd`, `git`, and [`git-lfs`](https://git-lfs.com) to verify and restore the export.

## 1. Create the bucket

Create a new bucket for the export. Set Object Ownership to **Bucket owner enforced**, so that your
account owns every object that we upload. The export checks this setting and stops before it writes
anything if the setting is different.

> [!WARNING]
>
> The commands in this step change settings for the whole bucket. Run them only on a new bucket that
> you create for the export. On a bucket that you already use, they can disable ACLs that other
> applications rely on, block existing public or cross-account access, and replace your lifecycle
> rules.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET and YOUR_REGION.
aws s3api create-bucket --bucket YOUR_BUCKET --region YOUR_REGION \
  --create-bucket-configuration LocationConstraint=YOUR_REGION

aws s3api put-bucket-ownership-controls --bucket YOUR_BUCKET \
  --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'

aws s3api put-public-access-block --bucket YOUR_BUCKET \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
```

> [!NOTE]
>
> In `us-east-1`, omit `--create-bucket-configuration`. S3 rejects that option in this region.

Add a lifecycle rule that removes incomplete multipart uploads. An interrupted upload can leave
parts behind, and S3 charges for them until they are removed.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET.
aws s3api put-bucket-lifecycle-configuration --bucket YOUR_BUCKET \
  --lifecycle-configuration '{"Rules":[{"ID":"abort-incomplete-uploads","Status":"Enabled","Filter":{},"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7}}]}'
```

Do not add a rule that expires or deletes objects in the export folder before you finish the
restore. Versioning is optional.

### Encrypt with your own KMS key (optional)

By default, the export encrypts objects with SSE-S3. To use SSE-KMS, create a customer-managed key
in the same region as the bucket:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_REGION.
aws kms create-key --region YOUR_REGION --description "Code Storage export" \
  --query KeyMetadata.Arn --output text
```

The command prints the key ARN. You send it to us in the next step.

## 2. Send us the destination

Email [support@pierre.co](mailto:support@pierre.co) with:

* The destination folder as an S3 URI: `s3://YOUR_BUCKET/YOUR_FOLDER/`
* The region of the bucket: `YOUR_REGION`
* For SSE-KMS only, the key ARN: `YOUR_KMS_KEY_ARN`

Use an empty folder, not the root of the bucket. The folder path cannot contain `.` or `..`
segments, empty segments such as `a//b`, or the characters `*`, `?`, `\`, and `%`.

We reply with a bucket policy, and with a key policy statement if you use SSE-KMS. Both have every
value filled in for your organization and folder.

## 3. Apply the policies

### Bucket policy

The bucket policy that we send has this form:

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PierreExportCheckOwnership",
      "Effect": "Allow",
      "Principal": { "AWS": "CODE_STORAGE_EXPORT_ROLE_ARN" },
      "Action": "s3:GetBucketOwnershipControls",
      "Resource": "arn:aws:s3:::YOUR_BUCKET",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalTag/eks-cluster-name": "CODE_STORAGE_CLUSTER",
          "aws:PrincipalTag/kubernetes-namespace": "CODE_STORAGE_NAMESPACE",
          "aws:PrincipalTag/kubernetes-service-account": "offboarding-export"
        }
      }
    },
    {
      "Sid": "PierreExportListFolder",
      "Effect": "Allow",
      "Principal": { "AWS": "CODE_STORAGE_EXPORT_ROLE_ARN" },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::YOUR_BUCKET",
      "Condition": {
        "StringLike": { "s3:prefix": "YOUR_FOLDER/*" },
        "StringEquals": {
          "aws:PrincipalTag/eks-cluster-name": "CODE_STORAGE_CLUSTER",
          "aws:PrincipalTag/kubernetes-namespace": "CODE_STORAGE_NAMESPACE",
          "aws:PrincipalTag/kubernetes-service-account": "offboarding-export"
        }
      }
    },
    {
      "Sid": "PierreExportWriteAndVerify",
      "Effect": "Allow",
      "Principal": { "AWS": "CODE_STORAGE_EXPORT_ROLE_ARN" },
      "Action": ["s3:PutObject", "s3:GetObject", "s3:AbortMultipartUpload"],
      "Resource": "arn:aws:s3:::YOUR_BUCKET/YOUR_FOLDER/*",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalTag/eks-cluster-name": "CODE_STORAGE_CLUSTER",
          "aws:PrincipalTag/kubernetes-namespace": "CODE_STORAGE_NAMESPACE",
          "aws:PrincipalTag/kubernetes-service-account": "offboarding-export"
        }
      }
    }
  ]
}
```

Check that `YOUR_BUCKET` and `YOUR_FOLDER` in the policy match your destination. The conditions
limit access to the export job for your organization. A job for another organization cannot use this
policy, even though it runs under the same role.

Save the policy as `code-storage-policy.json`. If the bucket has no policy, apply it as it is:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET.
aws s3api put-bucket-policy --bucket YOUR_BUCKET --policy file://code-storage-policy.json
```

If the bucket already has a policy, add our statements to it. A new policy replaces the old one
completely.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET.
aws s3api get-bucket-policy --bucket YOUR_BUCKET --query Policy --output text > current-policy.json
jq -s '.[0].Statement += .[1].Statement | .[0]' current-policy.json code-storage-policy.json \
  > merged-policy.json
aws s3api put-bucket-policy --bucket YOUR_BUCKET --policy file://merged-policy.json
```

### Key policy statement (SSE-KMS only)

For SSE-KMS, we also send a statement for the key policy. It lets the export job encrypt objects
with your key and read them back to verify them:

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "Sid": "PierreExportUseKey",
  "Effect": "Allow",
  "Principal": { "AWS": "CODE_STORAGE_EXPORT_ROLE_ARN" },
  "Action": ["kms:GenerateDataKey", "kms:Decrypt"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "aws:PrincipalTag/eks-cluster-name": "CODE_STORAGE_CLUSTER",
      "aws:PrincipalTag/kubernetes-namespace": "CODE_STORAGE_NAMESPACE",
      "aws:PrincipalTag/kubernetes-service-account": "offboarding-export"
    }
  }
}
```

Save it as `code-storage-key-statement.json` and add it to the existing key policy. Keep the
existing statements, so that your account can still use the key and decrypt the export.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_REGION and YOUR_KMS_KEY_ARN.
aws kms get-key-policy --region YOUR_REGION --key-id YOUR_KMS_KEY_ARN --policy-name default \
  --query Policy --output text \
  | jq --slurpfile statement code-storage-key-statement.json '.Statement += $statement' \
  > key-policy.json
aws kms put-key-policy --region YOUR_REGION --key-id YOUR_KMS_KEY_ARN --policy-name default \
  --policy file://key-policy.json
```

Tell us when you have applied the policies.

## 4. Stop using your repositories during the export

Agree on a start time with us. Before that time, stop all reads and changes to your repositories:
clones, fetches, pushes, reads and commits through the API, imports, syncs, and repository creation
or deletion. Let any operation in progress finish. Keep everything stopped until the export is
complete.

A read can move a repository out of cold storage while the export archives it, which can make the
export fail.

The export is not an atomic snapshot across repositories. A change during the export can invalidate
a repository's archive. If the set of repositories changes, the export stops, and we start a new
export in a new folder.

Do not change or delete files in the export folder while the export runs.

## 5. Track progress

`status.json` in the export folder shows progress. It updates after each batch of repositories.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET and YOUR_FOLDER.
aws s3 cp s3://YOUR_BUCKET/YOUR_FOLDER/status.json - \
  | jq '{status, repositories, completed_repositories}'
```

**`repositories.json` is the completion marker.** It appears only after every repository has a
verified archive and LFS copy. Until it exists, the export is not complete.

## 6. Download and verify

Download the export folder and check every file against its recorded SHA-256 checksum:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET and YOUR_FOLDER.
aws s3 sync s3://YOUR_BUCKET/YOUR_FOLDER/ ./code-storage-export/
cd code-storage-export

jq '{repositories, completed_at}' repositories.json

# Check the catalog files.
jq -r '.shards[] | "\(.sha256)  \(.key)"' repositories.json | shasum -a 256 -c -

# Check each archive, extract it, and check the Git objects.
mkdir -p ../restored
cat catalog/*.jsonl | while read -r line; do
  id=$(jq -r .id <<<"$line")
  key=$(jq -r .archive.key <<<"$line")
  echo "$(jq -r .archive.sha256 <<<"$line")  $key" | shasum -a 256 -c --quiet - \
    || echo "checksum mismatch: $id"
  mkdir -p "../restored/$id.git"
  zstd -qdc "$key" | tar -xf - -C "../restored/$id.git"
  git --git-dir="../restored/$id.git" fsck --full --no-progress >/dev/null \
    || echo "fsck failed: $id"
done

# Check each LFS object. Its file name is its SHA-256.
for f in repositories/*/lfs/*; do
  [ -e "$f" ] || continue
  [ "$(shasum -a 256 "$f" | cut -d' ' -f1)" = "$(basename "$f")" ] || echo "LFS mismatch: $f"
done
```

The export is complete and intact when `repositories` matches the number of repositories you expect
and the script prints no mismatch or failure. `git fsck` can print `dangling` lines, such as for a
deleted tag, and `notice: No default references` for an empty repository. These lines are not
errors.

## 7. Restore a repository

Each line in `catalog/*.jsonl` describes one repository:

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "id": "V1StGXR8_Z5jdHi6B-myT",
  "url": "team/project-alpha",
  "default_branch": "main",
  "archive": {
    "key": "repositories/V1StGXR8_Z5jdHi6B-myT/archive.tar.zst",
    "bytes": 48213,
    "sha256": "3f1c9a0e5b7d2f8c4a6e1b9d0c3f5a7e2d4b6c8a0f1e3d5c7b9a2e4f6d8c0b1a"
  },
  "lfs_prefix": "repositories/V1StGXR8_Z5jdHi6B-myT/lfs/",
  "lfs_objects": 2,
  "lfs_bytes": 10485760
}
```

`url` is the repository's name in Code Storage. Use it to match each `id` to a repository. Each
archive is a bare Git repository with all refs and history. The previous step extracted each one
into `restored/REPO_ID.git`.

To restore a repository, create an empty repository on your new Git host and push to it:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace REPO_ID and NEW_GIT_URL. Run from the folder that contains restored/ and code-storage-export/.
git --git-dir=restored/REPO_ID.git remote add destination NEW_GIT_URL
git --git-dir=restored/REPO_ID.git push --mirror destination
```

`git push --mirror` does not set the default branch on the new host. Set it to the catalog's
`default_branch` in your Git host's repository settings, or with its API. For example, on GitHub:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace OWNER/REPO and DEFAULT_BRANCH.
gh repo edit OWNER/REPO --default-branch DEFAULT_BRANCH
```

The archive can contain Code Storage–specific Git config. Review `restored/REPO_ID.git/config`
before you use the repository elsewhere.

### Restore LFS objects

If `lfs_objects` is more than `0`, copy the LFS objects into the restored repository and push them
to the new host. Review any `.lfsconfig` in the repository first, because it can name an old LFS
URL.

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace REPO_ID. Run from the folder that contains restored/ and code-storage-export/.
for f in code-storage-export/repositories/REPO_ID/lfs/*; do
  [ -e "$f" ] || continue
  oid=$(basename "$f")
  dir="restored/REPO_ID.git/lfs/objects/${oid:0:2}/${oid:2:2}"
  mkdir -p "$dir" && cp "$f" "$dir/$oid"
done
git --git-dir=restored/REPO_ID.git lfs push --all destination
```

Clone the repository from the new host and check its branches, tags, history, and LFS files before
you continue. `RESTORE.md` in the export folder has the same steps.

## 8. Remove our access

After you verify the export, remove our statements from the bucket policy:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET.
aws s3api get-bucket-policy --bucket YOUR_BUCKET --query Policy --output text \
  | jq 'del(.Statement[] | select((.Sid // "") | startswith("PierreExport")))' > cleaned-policy.json
aws s3api put-bucket-policy --bucket YOUR_BUCKET --policy file://cleaned-policy.json
```

If our statements were the only statements in the policy, delete the policy instead:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_BUCKET.
aws s3api delete-bucket-policy --bucket YOUR_BUCKET
```

For SSE-KMS, remove the `PierreExportUseKey` statement from the key policy:

```bash theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Replace YOUR_REGION and YOUR_KMS_KEY_ARN.
aws kms get-key-policy --region YOUR_REGION --key-id YOUR_KMS_KEY_ARN --policy-name default \
  --query Policy --output text \
  | jq 'del(.Statement[] | select(.Sid == "PierreExportUseKey"))' > key-policy.json
aws kms put-key-policy --region YOUR_REGION --key-id YOUR_KMS_KEY_ARN --policy-name default \
  --policy file://key-policy.json
```

Then tell us that you have verified the export. We delete your repositories from Code Storage only
after you confirm the export and a deletion date with us.

## What the export contains

| Path | Contents |
| - | - |
| `repositories.json` | Completion marker: repository count and catalog file list |
| `catalog/000000.jsonl` | One line for each repository, up to 1,000 lines for each file |
| `repositories/<id>/repository.json` | The catalog line for this repository |
| `repositories/<id>/archive.tar.zst` | A bare Git repository with all refs and history |
| `repositories/<id>/lfs/<oid>` | Each LFS object, named by its SHA-256 |
| `RESTORE.md` | Download, verify, and restore steps |
| `status.json` | Progress of the export |
| `.export/` | State that the export uses to resume after an interruption |

The export contains Git repositories and LFS objects. It does not contain organization settings,
access keys, webhooks, or other configuration.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.