# gcloud CLI Cheat Sheet — gsutil: The Legacy Cloud Storage CLI

> **Tool:** gsutil (legacy Cloud Storage CLI)
> **Category:** Cloud CLIs
> **Verified against:** gsutil 5.35, boto 2.49.0 (bundled with Google Cloud SDK 553.0.0), flags verified via
> `gsutil help options`, `gsutil help rewrite`, `gsutil help notification`, `gsutil help signurl`, `gsutil
> help du`, a real local `gsutil version -l` confirming the `.boto` config search path, and a comparison
> against `gcloud storage --help` (page `03`) run locally, 2026-09-18
> **Official docs:** https://cloud.google.com/storage/docs/gsutil

## What it is and where it fits 🎯

`gsutil` was Cloud Storage's original command-line tool, and `gcloud storage` (page `03`) is its official
replacement, faster, better integrated with `gcloud`'s own auth/config, and the one Google now recommends
for new scripts. This page exists because `gsutil` is still installed by default, still runs plenty of
existing production scripts and Terraform provisioners, and still covers a handful of things `gcloud
storage` genuinely doesn't yet, not because it's the tool to reach for first. Read page `03` for everyday
object operations; read this page to understand `gsutil`'s own config file, the commands it still uniquely
offers, and how to read (or migrate) a script written against it.

## Core concepts: where gsutil's config actually lives

```mermaid
flowchart TD
    Boto["~/.boto<br/>(gsutil's own config file)"] --> Creds["Credentials section<br/>(legacy, or delegates to gcloud)"]
    Boto --> Perf["[GSUtil] section<br/>parallel_thread_count, etc."]
    Legacy["~/.config/gcloud/legacy_credentials/<br/>&lt;account&gt;/.boto"] -.->|"gsutil also checks this path"| Boto
    AWS["~/.aws/credentials"] -.->|"checked for S3 interoperability"| Boto

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef muted fill:#eaeef1,stroke:#c3ccd4,color:#10161c
    class Boto info
    class Legacy,AWS muted
```

Unlike `gcloud storage`, which reads `gcloud`'s own config directory (page `00`) directly, `gsutil` is built
on the third-party `boto` library and keeps its **own** separate config file, `~/.boto`, plus a
per-account fallback under `~/.config/gcloud/legacy_credentials/<account>/.boto` that `gcloud auth login`
writes automatically so `gsutil` has *something* to authenticate with even without its own `gsutil config`
step, confirmed directly against a real `gsutil version -l` invocation's reported config search path. This
is the practical reason a freshly-authenticated `gcloud` session can run `gsutil` commands immediately with
no separate `gsutil config` step, the legacy `.boto` credential file already exists by the time `gcloud
auth login` finishes.

## Config file: `~/.boto`

```ini
[Credentials]
# Populated automatically by `gcloud auth login` — rarely hand-edited directly

[Boto]
# proxy = myproxy.example.com:8080

[GSUtil]
parallel_thread_count = 10
parallel_process_count = 4
parallel_composite_upload_threshold = 150M
use_magicfile = True

[OAuth2]
```

`[GSUtil]` is the section worth hand-editing, `parallel_composite_upload_threshold` is what actually
controls whether a large upload gets split into concurrent chunks (composite upload) or sent as one
sequential stream, tuning it up or down directly trades upload speed against the API-request overhead of
managing more, smaller pieces. Regenerate or repair the file with `gsutil config -n` if it's ever missing
or corrupted, that flag skips the (obsolete, `gcloud`-superseded) interactive OAuth authorization step
and only rebuilds the config scaffolding.

## Commands that still only exist in gsutil

```bash
gsutil rewrite -k gs://my-bucket/**    # re-encrypt every object under a bucket with the bucket's CURRENT default KMS key
gsutil rewrite -s STANDARD gs://my-bucket/old-logs/**   # bulk storage-class change without a full copy

gsutil notification create -f json -t my-pubsub-topic -e OBJECT_FINALIZE gs://my-bucket
gsutil notification list gs://my-bucket

gsutil du -sh gs://my-bucket   # human-readable total size, no equivalent gcloud storage flag as of this version
gsutil hash -h gs://my-bucket/file.zip   # compute/display CRC32C and MD5 without downloading the object
```

`rewrite -k` is the bulk key-rotation tool, when a KMS key (page `08`) is rotated, existing objects
encrypted under the old key version stay encrypted under it until something actually rewrites them,
`gsutil rewrite -k` is that something, applied in bulk across a whole bucket or prefix rather than one
object at a time. `notification create` predates Cloud Storage's newer Eventarc-based triggers (page `05`)
and is still the direct path for a plain Pub/Sub notification on object changes without going through
Eventarc's extra routing layer at all.

## Parallel operations: the `-m` flag

```bash
gsutil -m cp -r ./local-dir gs://my-bucket/path/
gsutil -m rsync -r ./local-dir gs://my-bucket/path/
```

`-m` is a top-level `gsutil` flag (comes *before* the subcommand, not after), enabling multi-threaded/
multi-process transfers, the single biggest lever for transfer speed on a large batch of files, and one
`gcloud storage` actually replicates with its own equivalent parallelism by default, without needing an
explicit flag, one of the concrete reasons `gcloud storage` is faster out of the box for the same operation.

## Composing objects server-side

```bash
gsutil compose gs://my-bucket/part1 gs://my-bucket/part2 gs://my-bucket/part3 gs://my-bucket/combined
```

`compose` concatenates existing objects into a new one entirely server-side, with no data round-tripping
through the local machine, the mechanism underneath parallel composite uploads (page `03`'s large-file
upload path splits a file into parts, uploads each concurrently, then composes them), and occasionally
useful directly for stitching together log shards or chunked export files without downloading anything.

## Performance diagnostics: `perfdiag`

```bash
gsutil perfdiag -n 20 -s 10M gs://my-bucket
gsutil perfdiag -o results.json -t write,read,metadata gs://my-bucket
```

`perfdiag` runs a structured suite of upload/download/metadata-latency tests against a real bucket and
reports throughput and latency numbers, genuinely useful evidence when a team suspects a transfer speed
problem is network/region-related rather than caused by application code, `-o` saves full results to share
with Google Cloud Support if a ticket is warranted, they specifically ask for this file's format.

**Illustrative output** (exact numbers vary by network path and region, this is a representative shape, not
a captured run):

```
Operation           Bytes    Count  Avg Time  Throughput
---------            -----    -----  --------  ----------
write                10.0M      20    0.412s     24.3 MB/s
read                 10.0M      20    0.187s     53.5 MB/s
metadata                 -      20    0.041s     -

TCP connect times:  min=0.021s  max=0.089s  avg=0.043s
```

A large gap between `write` and `read` throughput, or an unusually high TCP connect time, is the signal
worth escalating, a healthy bucket in the same region as the test client typically shows read throughput
comfortably higher than write, since reads can be served from more replicas than a write has to confirm
against.

## Migrating a script from gsutil to gcloud storage

| gsutil | gcloud storage equivalent |
|---|---|
| `gsutil cp src dst` | `gcloud storage cp src dst` |
| `gsutil -m rsync -r src dst` | `gcloud storage rsync src dst` (parallelism is automatic) |
| `gsutil ls -l gs://bucket` | `gcloud storage ls -l gs://bucket` |
| `gsutil mb gs://bucket` | `gcloud storage buckets create gs://bucket` |
| `gsutil rb gs://bucket` | `gcloud storage buckets delete gs://bucket` |
| `gsutil iam ch user:x:objectViewer gs://bucket` | `gcloud storage buckets add-iam-policy-binding gs://bucket --member=user:x --role=roles/storage.objectViewer` |
| `gsutil signurl key.json gs://bucket/obj` | `gcloud storage sign-url gs://bucket/obj --private-key-file=key.json` |
| `gsutil rewrite -k`, `gsutil notification`, `gsutil du`, `gsutil hash` | no direct `gcloud storage` equivalent as of this version |

Most day-to-day object/bucket operations map one-to-one, often literally just swapping the binary name and
`gsutil`'s flag style for `gcloud storage`'s, per page `03`'s own coverage of the modern surface. The bottom
row is the genuine, current reason to keep `gsutil` installed and reachable rather than treating it as fully
retired.

> [!NOTE]
> Google has not announced a retirement date for `gsutil` as of this writing, it continues to receive
> updates, but all new Cloud Storage features are documented against `gcloud storage` first, and `gsutil`
> sometimes lags behind by a release or two on a brand-new capability. Treat `gsutil` as stable-but-legacy,
> not deprecated-and-unsafe.

## Real-world scenario: rotating a KMS key across an entire bucket's existing objects

A security review requires re-encrypting every object in a bucket after rotating its default KMS key (page
`08`), since a KMS key rotation alone only affects *new* writes, not objects already stored:

```bash
gcloud storage buckets update gs://my-bucket \
  --default-encryption-key=projects/my-project-id/locations/us-central1/keyRings/app-keyring/cryptoKeys/app-encryption-key

gsutil -m rewrite -k -r gs://my-bucket/**
```

The bucket-level default key controls what *new* objects are encrypted with; `rewrite -k` is what actually
walks the existing objects and re-encrypts each one under whatever the bucket's current default key is at
the time it runs, `-m` parallelizes it across a large bucket rather than processing objects one at a time.

## Real-world scenario: auditing storage cost by directory prefix before a cleanup

- [ ] `gsutil du -sh gs://my-bucket/*/ ` to see per-prefix totals at a glance, faster than paging through
      `ls -l` output and summing manually for a bucket with thousands of objects
- [ ] Identify the largest prefixes and confirm with the team whether they're still needed before deleting
      anything
- [ ] Add a lifecycle rule (page `03`) for the prefixes that are needed but aging, rather than a one-time
      manual cleanup that has to be repeated again in six months
- [ ] Re-run `du -sh` after the lifecycle rule has had time to act, confirming it actually reduced the
      footprint as expected

## Real-world scenario: a Terraform provisioner still shelling out to gsutil

An older Terraform module uses a `local-exec` provisioner to sync static assets after creating a bucket,
predating the site's current preference (page `03`) for handling this in the deploy pipeline instead:

```hcl
resource "google_storage_bucket" "site" {
  name     = "my-static-site"
  location = "US"
}

resource "null_resource" "sync_assets" {
  provisioner "local-exec" {
    command = "gsutil -m rsync -r ./dist gs://${google_storage_bucket.site.name}"
  }
  depends_on = [google_storage_bucket.site]
}
```

> [!TIP]
> A `local-exec` provisioner shelling out to either `gsutil` or `gcloud storage` runs only on `terraform
> apply` from whatever machine executes it, not on every deploy, it's a reasonable stopgap for a one-time
> asset seed at bucket creation, but a genuine CI/CD deploy pipeline (this page's own recipe below, or
> page `03`'s) is the better home for anything that needs to run on every release, Terraform state has no
> way to know the *remote* bucket contents changed and won't re-run this step on its own.

## CI/CD integration recipe: legacy pipeline still using gsutil, authenticated via WIF

```yaml
# .github/workflows/legacy-sync.yml
name: Sync build output (legacy gsutil pipeline)
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
          service_account: ci@my-project-id.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v2
      - run: gsutil -m rsync -r ./out gs://my-bucket
```

An existing pipeline that already works doesn't need to be rewritten onto `gcloud storage` just for its own
sake, `google-github-actions/auth` populates the same shared credential store either tool reads, `gsutil`
authenticates through it exactly like `gcloud storage` would; migrate the *commands* opportunistically
(the next time that pipeline needs a real change) rather than as a dedicated, disruptive rewrite effort.

## Common pitfalls

- **Placing `-m` after the subcommand instead of before it.** `gsutil cp -m ...` is a syntax error; `-m`
  is a top-level flag: `gsutil -m cp ...`.
- **Assuming a KMS key rotation alone re-encrypts existing objects.** It only changes what *new* writes use;
  `rewrite -k` is required to touch existing ones, see the real-world scenario above.
- **Hand-editing the `[Credentials]` section of `.boto` instead of just re-running `gcloud auth login`.**
  The credentials section is machine-managed; editing it directly is far more error-prone than
  re-authenticating through `gcloud`.
- **Writing brand-new scripts against `gsutil` in 2026.** `gcloud storage` (page `03`) is the current
  recommended default; reach for `gsutil` specifically for the commands in the migration table with no
  `gcloud storage` equivalent yet, not out of habit.
- **Forgetting `parallel_composite_upload_threshold` changes upload *shape*, not just speed.** A composite
  upload creates the final object by composing several component objects server-side; a process that reads
  raw object metadata expecting a single simple upload can occasionally behave unexpectedly against one.

## Exit codes

`0` success, non-zero on any API/validation error or a partial failure during a multi-file `-m` operation,
a parallelized `cp`/`rsync` that fails on some files but succeeds on others still reports a non-zero exit
overall, `gsutil`'s own summary output (not just the exit code) is what shows exactly which files failed.

## When to reach for something else

For everything not in the migration table's bottom row, default to `gcloud storage` (page `03`) instead,
it's faster, shares `gcloud`'s own config and auth directly with no separate `.boto` file to maintain, and
is the surface Google documents new features against first. For declarative, reviewable bucket
provisioning, prefer Terraform's `google_storage_bucket` resource over either CLI, consistent with the IaC
guidance on every earlier page.
