How to8 min readOctober 1, 2026

aws s3 cp examples: copy files and folders, --recursive, and checking with aws s3 ls

How aws s3 cp names the objects it writes, what --recursive copies, filtering with --exclude and --include, server-side copies between buckets, changing metadata in place, and using aws s3 ls to check the result.

JeVaughn Ferguson
Founder, developer
The short version

aws s3 cp copies whatever you point it at and overwrites what is already there. A trailing slash on the destination keeps the file name, --recursive copies a folder's contents rather than the folder, and filters run in order with the last match winning. S3-to-S3 copies happen server side, metadata changes need a copy onto itself with --metadata-directive REPLACE, and aws s3 ls --recursive --summarize is the quick check that the right number of objects landed.

aws s3 cp copies one file, or with --recursive a whole directory or prefix, between your machine and S3 or between two S3 locations. Unlike sync, it never compares anything: every file it selects is copied, and an object already at the destination key is overwritten.

That makes it the right command for a one-off copy, a single download in a script, or a deliberate overwrite. This guide covers how cp decides the destination key, what --recursive does with folders, the flags that matter, and how to check the result with aws s3 ls.

The trailing slash decides the key

S3 has no folders, only keys. When you copy one file, the destination you type becomes the key unless it ends in a slash. With a trailing slash, cp treats the destination as a prefix and appends the file name.

Downloads work the same way in reverse: a local destination that is an existing directory, or ends in a slash, keeps the object's name. Anything else is used as the new file name.

# Key becomes reports/q3.pdf: the trailing slash keeps the file name.
aws s3 cp q3.pdf s3://amzn-s3-demo-bucket/reports/

# Key becomes "reports" with no extension. Usually a mistake.
aws s3 cp q3.pdf s3://amzn-s3-demo-bucket/reports

# Download into the current directory, keeping the name.
aws s3 cp s3://amzn-s3-demo-bucket/reports/q3.pdf .

Copying a folder with --recursive

Without --recursive, cp copies one file. With it, cp copies everything under a local directory or an S3 prefix, including subdirectories, and keeps the relative paths.

It copies the contents, not the directory itself. aws s3 cp ./reports s3://amzn-s3-demo-bucket/reports/ --recursive writes reports/q3.pdf, not reports/reports/q3.pdf, so name the destination prefix you want. Empty local directories are not uploaded, because there is no file to make a key from.

# Upload a folder tree under a prefix.
aws s3 cp ./reports s3://amzn-s3-demo-bucket/reports/ --recursive

# Download a prefix to a local folder (created if it does not exist).
aws s3 cp s3://amzn-s3-demo-bucket/reports/ ./reports --recursive

# Preview what a recursive copy would do without copying anything.
aws s3 cp ./reports s3://amzn-s3-demo-bucket/reports/ --recursive --dryrun

--exclude and --include

Filters only apply with --recursive, and they follow the same rule as sync: everything is included by default, filters are evaluated in the order you write them, and the last match wins. To copy only certain files, exclude everything first and include afterwards.

Patterns are matched against the path relative to the source, so --exclude "drafts/*" works and an absolute local path does not.

# Only PDFs from a prefix.
aws s3 cp s3://amzn-s3-demo-bucket/finance/ ./finance --recursive \
  --exclude "*" --include "*.pdf"

# Everything except temp files and one subfolder.
aws s3 cp ./finance s3://amzn-s3-demo-bucket/finance/ --recursive \
  --exclude "*.tmp" --exclude "archive/*"

Copying between buckets, and changing metadata in place

When both sides are S3 locations, cp asks S3 to copy the object server side. The data does not pass through your machine, which makes bucket-to-bucket and cross-account copies fast from a laptop. The credentials you run with need read access on the source and write access on the destination, and --source-region helps when the two buckets are in different Regions.

S3 objects cannot be edited, so changing an object's content type, cache headers or user metadata means copying it onto itself. Pass the new values with --metadata-directive REPLACE: metadata you do not restate is dropped, so include everything you want to keep.

# Copy a prefix to a bucket in another Region, server side.
aws s3 cp s3://amzn-s3-demo-bucket/reports/ s3://amzn-s3-demo-archive/reports/ \
  --recursive --source-region us-east-1 --region eu-west-1

# Fix a wrong content type by copying the object onto itself.
aws s3 cp s3://amzn-s3-demo-bucket/site/index.html s3://amzn-s3-demo-bucket/site/index.html \
  --content-type "text/html; charset=utf-8" --cache-control "max-age=300" \
  --metadata-directive REPLACE

Flags worth knowing

Most of these also work with sync. They apply only to the objects this run writes.

  • --storage-class: write straight to STANDARD_IA, INTELLIGENT_TIERING, GLACIER_IR or another class.
  • --sse aws:kms with --sse-kms-key-id: encrypt with a specific KMS key instead of the bucket default.
  • --content-type, --cache-control, --content-disposition: set headers a browser will see when it downloads the object.
  • --acl: only works when the bucket still allows ACLs; buckets with Object Ownership set to bucket owner enforced reject it.
  • A source or destination of - streams from stdin or to stdout, for example to pipe a database dump straight into S3; add --expected-size for streams over 50 GB.
  • --only-show-errors and --no-progress: keep scheduled-job logs readable.

Checking the result with aws s3 ls

aws s3 ls with no arguments lists your buckets. Given a bucket or prefix, it lists one level: objects with their time and size, and sub-prefixes as PRE lines. The path is a prefix match, so s3://amzn-s3-demo-bucket/reports without a slash also matches reports-2025.csv; add the slash to list inside the folder.

--recursive lists every key underneath, and --human-readable with --summarize adds sizes in KB, MB or GB and a total object count and size at the end. On a bucket with millions of objects that is a long series of LIST requests, so scope it to the prefix you care about.

# What is inside the reports folder, one level deep.
aws s3 ls s3://amzn-s3-demo-bucket/reports/

# Count and size of everything a recursive copy wrote.
aws s3 ls s3://amzn-s3-demo-bucket/reports/ --recursive --human-readable --summarize

Exit codes in scripts

aws s3 cp returns 0 when everything copied. It returns 1 when at least one transfer failed, and 2 when some files were skipped, for example local files that disappeared or could not be read, while the rest copied. A script that only checks for a non-zero status treats a partial copy as a failure, which is usually what you want; one that ignores the status will carry on after half a folder.

Two other habits save trouble. cp overwrites without asking, so keep versioning on for buckets that scripts write to. And if the same copy runs every night, switch to sync, which only sends what changed.

Try it in BucketDesk

Starter is free. Deploy a scoped role with CloudFormation, sign in, and browse, without handing anyone an access key.

Connect a bucket

Primary sources

Discussion

0 comments · open to guests · moderated
Comments appear after a quick review.

Liked this? Get the next article by email. No schedule, no filler, one click to leave.

Keep reading

All writing →