@aiconnect/s3cli
v1.5.1
Published
Simple CLI for S3 operations using AWS SDK. Compatible with Supabase Storage, AWS S3, MinIO and other S3-compatible providers.
Maintainers
Readme
s3cli
Agent-native CLI for S3 operations using AWS SDK. Compatible with Supabase Storage, AWS S3, MinIO and other S3-compatible providers.
Every command emits a stable, structured JSON envelope when invoked from a pipe or subprocess, and a human-friendly output when used in a TTY — so the same binary is consumed by humans in a terminal and by LLM agents over stdout.
Installation
npm install
npm run buildFor development, you can use npm run start -- <command> instead of npm link + s3cli.
Configuration
The quickest way to get started is with the interactive init command:
s3cli initThis creates $XDG_CONFIG_HOME/s3cli/.env (defaults to ~/.config/s3cli/.env) with your credentials, with permissions set to 0600.
Config resolution order
--env-file <path>— custom.envfile passed via CLI flag (highest priority).--profile <slug>— named profile loaded from$XDG_CONFIG_HOME/s3cli/<slug>.env(with legacy fallback at~/.s3cli/<slug>.env). See Profiles below.$XDG_CONFIG_HOME/s3cli/.env— XDG-compliant location (default~/.config/s3cli/.env).~/.s3cli/.env— legacy path; loading from here emits a deprecation warning on stderr.- Shell environment variables — already exported in the shell (lowest priority, not overwritten by dotenv).
--env-file and --profile are mutually exclusive — combining them exits with USAGE_ERROR.
If no config is found, s3cli will tell you to run s3cli init.
Profiles
Use --profile <slug> to switch between named configurations (e.g., production, staging, MinIO local) without editing files or repeating --env-file:
s3cli ls --profile staging
s3cli upload local.txt remote.txt --profile productionEach profile is a separate .env file with the same schema as the default config, named <slug>.env and stored in the same directory:
# Create a "staging" profile by copying the default config:
cp ~/.config/s3cli/.env ~/.config/s3cli/staging.env
$EDITOR ~/.config/s3cli/staging.env # adjust endpoint, bucket, credentialsSlug rules: must match [a-zA-Z0-9._-]+ (letters, digits, dot, underscore, hyphen). Path separators, spaces, and empty strings are rejected with USAGE_ERROR. If the profile file does not exist at either the XDG path or the legacy ~/.s3cli/<slug>.env, the command exits with USAGE_ERROR rather than falling back to the default .env.
Migrating from the legacy path
If you still have config at ~/.s3cli/.env, migrate it with:
s3cli init --migrate-configThis copies ~/.s3cli/.env to ~/.config/s3cli/.env (or $XDG_CONFIG_HOME/s3cli/.env when set) and leaves the legacy file in place. Failures (no legacy file, or XDG file already present) exit with code 30.
Breaking change (v1.3.0): Local
.envfiles in the current directory are no longer loaded automatically. If you relied on this behavior, use--env-file ./.envexplicitly or move your config to~/.config/s3cli/.env.
Manual setup
You can also create the .env file manually:
S3_ENDPOINT=https://your-project.supabase.co/storage/v1/s3
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_USE_SSL=true
S3_BUCKET=your-bucket
S3_REGION=eu-central-1
S3_PATH_STYLE=true
S3CLI_DEFAULT_EXPIRY=3600To inspect the active configuration (with secrets masked):
s3cli config showOutput format
All commands accept a global --format <json|human> flag. The default is TTY-aware: human when stdout is a terminal, json when it's piped, redirected, or captured by a subprocess. Pass --format explicitly to override.
- stdout — exclusive for the structured response (JSON envelope or one-line human message).
- stderr — operational logs, progress lines, and deprecation warnings.
This means s3cli put a.txt remote/a.txt --format json | jq . is always safe.
Success envelope
{
"_apiVersion": "1.0",
"ok": true,
"command": "put",
"result": {
"bucket": "my-bucket",
"key": "remote/a.txt",
"etag": "\"abc123\"",
"size": 1024,
"contentType": "application/json",
"versionId": null
}
}Error envelope
{
"_apiVersion": "1.0",
"ok": false,
"command": "put",
"errorCode": "PRECONDITION_FAILED",
"message": "If-Match precondition did not match current ETag.",
"remediation": "Call 's3cli head <remote>' to get the current ETag and retry.",
"context": {
"remote": "skills/onboarding.json",
"expectedETag": "\"abc123\""
}
}Exit codes
| Code | Name | When |
|------|------|------|
| 0 | SUCCESS | Command completed |
| 2 | USAGE_ERROR | Invalid arguments or local file missing |
| 30 | PRECONDITION_FAILED | Conditional write failed (--if-match/--if-none-match) or precondition for init --migrate-config not met |
| 40 | IO_ERROR | Generic S3 / network / config error |
| 50 | NOT_FOUND | Object or source key does not exist |
Remote argument syntax
Most commands accept the remote target either as a plain key or as a full s3://<bucket>/<key> URI:
# Equivalent — uses the configured S3_BUCKET (or --bucket flag)
s3cli get path/file.txt
s3cli --bucket my-bucket get path/file.txt
# URI form — explicit bucket, ignores S3_BUCKET
s3cli get s3://my-bucket/path/file.txt
# Cross-bucket copy with URIs on both sides
s3cli cp s3://source-bucket/x.txt s3://dest-bucket/y.txtCommands that accept URIs: get, put, rm, head, url, copy/cp, upload. (ls does not — it operates on a prefix and uses the configured bucket.)
Rules:
- The URI bucket has precedence over
--bucketandS3_BUCKET. If--bucketis also passed with a different value, the command exits withUSAGE_ERROR. S3_BUCKETis optional in the config file when you always provide a URI or--bucket. Commands that need a bucket and cannot resolve one from any source exit withUSAGE_ERROR.- Other schemes (
https://,s3a://, …) are not parsed — they are treated as literal keys. - Percent-encoded characters in the key are preserved literally (e.g.
s3://bucket/a%20b.txt⇒ keya%20b.txt).
Usage
List files (ls)
# List all files (default: first 100)
s3cli ls
# Filter by prefix
s3cli ls folder/
# Local pagination
s3cli ls --limit 50 --offset 50
# No limit (all files)
s3cli ls --limit 0
# Directory-style listing with delimiter
s3cli ls skills/ --delimiter "/"
# Server-side pagination via S3 continuation token
s3cli ls --continuation-token "<token-from-previous-page>"In --format json, the response includes objects, total, isTruncated, and (when applicable) commonPrefixes and nextContinuationToken.
Upload (upload) — simple uploads
s3cli upload file.txt backup/file.txtupload auto-detects Content-Type by extension and supports public-read ACL:
# Override the Content-Type
s3cli upload file.txt backup/file.txt --content-type text/plain
# Public-read ACL
s3cli upload file.txt backup/file.txt --publicPut (put) — uploads with conditional writes & metadata
put is the agent-oriented upload primitive. Use it when you need conditional writes or custom metadata.
# Basic upload (equivalent to `upload`, but no --public)
s3cli put file.txt backup/file.txt
# Override Content-Type
s3cli put file.txt backup/file.txt --content-type text/plain
# Conditional update — only overwrite if the remote ETag still matches
s3cli put file.txt backup/file.txt --if-match '"abc123"'
# Conditional create — only upload if the key does NOT exist
s3cli put file.txt backup/file.txt --if-none-match
# Custom metadata (key=value, comma-separated)
s3cli put file.txt backup/file.txt --metadata 'publishedBy=agent-001,version=2'--if-match and --if-none-match are mutually exclusive. On precondition failure, put exits with code 30 and an errorCode of PRECONDITION_FAILED or KEY_ALREADY_EXISTS.
Reading content from stdin or another file (--body)
When --body is set, <source> becomes the S3 key and the upload content comes from the path passed to --body (or from stdin if --body -):
# Upload from stdin (e.g. piping JSON output from another command)
echo '{"hello":"world"}' | s3cli put skills/hello.json --body -
# Upload from an arbitrary file (key derived from the S3 path, not from --body)
s3cli put assets/app.js --body /tmp/built-app.jsNotes:
<remote>positional is rejected when--bodyis used — the key is<source>. Combining both exits withUSAGE_ERROR(code2).Content-Typeis auto-detected from the S3 key extension (not from the--bodypath). Override with--content-typewhen needed.--body -buffers stdin in memory up to 50 MB. For larger payloads, write to a temp file and pass it via--body <path>. The limit is overridable withS3CLI_STDIN_MAX_BYTES=<bytes>; exceeding it exits withUSAGE_ERROR.
Head (head) — metadata + ETag without download
s3cli head backup/file.txtReturns object metadata, ETag, size, content-type, last-modified, custom metadata, and version ID. Missing keys are not errors — they return ok: true with result.exists: false, which is the natural shape for "check then act" agent flows.
Copy (copy) — server-side copy
# Copy within the configured bucket
s3cli copy current/SKILL.md previous/SKILL.md
# Copy from a different source bucket
s3cli copy src.txt dst.txt --source-bucket other-bucket
# Replace metadata during copy
s3cli copy a.txt b.txt --metadata-directive REPLACE --metadata 'owner=team-x'--metadata requires --metadata-directive REPLACE. A missing source key exits with code 50 (NOT_FOUND).
Download (get)
# Download keeping original name
s3cli get backup/file.txt
# Download with new name
s3cli get backup/file.txt restored-file.txt
# Conditional download — fails with code 30 if the remote ETag drifted
s3cli get backup/file.txt --if-match '"abc123"'Delete (rm)
s3cli rm backup/file.txtBy default, deleting a missing key is a noop (success with result.action: "noop"), which keeps agent retries idempotent. To treat a missing key as an error instead:
s3cli rm backup/file.txt --not-found-errorGenerate signed URL (url)
# URL valid for 1 hour (default)
s3cli url backup/file.txt
# URL valid for 24 hours (86400 seconds)
s3cli url backup/file.txt 86400
# Only the URL (no message, ideal for pipes)
s3cli url backup/file.txt --quiet
# Automatically shortened URL (requires S3CLI_SHORTENER_CMD)
s3cli url backup/file.txt --shorten
# Combined: short and clean URL
s3cli url backup/file.txt -qs
# Unsigned URL (for public buckets / Supabase Storage)
s3cli url backup/file.txt --unsignedIn --format json, the response includes url, expiresInSeconds, and expiresAt (ISO 8601). Unsigned URLs return unsigned: true instead of an expiry.
URL Shortener
To use the --shorten flag, configure the S3CLI_SHORTENER_CMD environment variable:
# In .env file
S3CLI_SHORTENER_CMD=shortener
# Or export directly
export S3CLI_SHORTENER_CMD=shortenerThe command should accept a URL as argument and output the shortened URL to stdout. Example with shortener:
shortener "https://example.com/long/url"
# Output: https://short.io/abc123
s3cli url file.txt --shorten
# Output: https://short.io/xyz789Help
Each command supports two help formats:
# Classic Unix-style help (default in TTY)
s3cli put --help
# Markdown-formatted help inside a JSON envelope (agent-friendly)
s3cli put --help --format jsonCommands
| Command | Description |
|---------|-------------|
| init [--migrate-config] | Interactive setup; --migrate-config moves ~/.s3cli/.env to the XDG path |
| config show | Print active configuration (secrets masked) |
| ls [prefix] | List files (--limit, --offset, --delimiter, --continuation-token) |
| upload <local> [remote] | Simple upload (--content-type, --public for public-read ACL) |
| put <source> [remote] | Upload with conditional writes / metadata (--if-match, --if-none-match, --metadata, --content-type). With --body <path\|->, <source> becomes the S3 key (--body - reads stdin, max 50MB) |
| head <remote> | Metadata + ETag without downloading |
| copy <src> <dst> | Server-side copy (--source-bucket, --metadata-directive, --metadata) |
| get <remote> [local] | Download a file (--if-match) |
| rm <remote> | Delete a file (--not-found-error) |
| url <remote> [expiry] | Generate a signed URL (--unsigned, --quiet, --shorten) |
upload and put coexist: use upload for the simple "send this file" path (with --public ACL support), and put whenever you need conditional writes or custom metadata.
Global flags
| Flag | Description |
|------|-------------|
| --bucket <name> | Override the default bucket from config |
| --env-file <path> | Load configuration from a custom .env file |
| --profile <slug> | Load configuration from named profile (<slug>.env in config dir); mutually exclusive with --env-file |
| --format <json\|human> | Output format (default: human in TTY, json when piped) |
Global Installation
To use s3cli as a global command:
npm run build
npm linkThen:
s3cli ls
s3cli put file.txt remote/file.txt --if-none-match
s3cli url remote/file.txt 3600License
MIT
