A terminal user's next interface to object storage
I live in the terminal. If I can't do it from a shell prompt, it basically doesn't exist. In order to make Tigris easier to use for developers like me, we've made a brand new Tigris CLI that lets you manage your buckets directly from your shell. Take a gander:
$ tigris cp AGENTS.md t3://xedn/AGENTS.md
Uploading object...
┌───────────┬────────┬──────────────┬───────────────────────┐
│ Path │ Size │ Content-Type │ Modified │
├───────────┼────────┼──────────────┼───────────────────────┤
│ AGENTS.md │ 4.2 KB │ text/plain │ Feb 5, 2026, 12:56 PM │
└───────────┴────────┴──────────────┴───────────────────────┘
That copied my local AGENTS.md file to
my bucket in Tigris. As you can see, this
new Tigris CLI gives you all the buckety goodness you know and love anywhere
with a shell prompt. Even Windows. It's just an npm install away:
npm install -g @tigrisdata/cli
Then tigris login and you're good to go!
Wait, didn't you have a CLI already?
Fun fact: Tigris wasn't always an object storage company. We used to be a
database company that made a database similar to DynamoDB. When we made that, we
had a separate tigris command that let you interact with the database from the
command line. Given that product is cancelled and offline, we've deprecated that
old tigris command in favour of this new one that lets you interact with
object storage.
What's different about the tigris command?
The UNIX philosophy states that each command should be as simple as possible
so that complex functionality can be created by chaining programs together
like how 8 simple ingredients at Taco Bell can assemble a wide array of menu options.
Each of the subcommands in the tigris command follows this rule. Here are some
examples so you get the idea:
tigris login: Authenticates you to Tigris with a temporary access key.tigris buckets list: Lists the buckets in your current Tigris organization.tigris objects delete: Delete an object.
At a high level, tigris uses this pattern:
tigris DOMAIN OPERATION <ARGUMENT> --flags
This maps really cleanly to how you'd intuitively expect it to work:
- Domain: the area of concern: objects, buckets, credentials, forks, snapshots, etc.
- Operation: is the verb:
list,get,put,delete,copy,move. - Arguments: what comes after. You'll intuit your way through the rest of
the program after one command, ideally so natural that you can squint at a
tigriscommand once and then figure your way through the rest of the program.
One neat side effect of this being natural for humans is that it's also natural for our artificial coworkers to figure out thanks to them being trained on so much CLI tool documentation!
❯ Try running tigris objects put --help and copy yeetfile.js to the skynet bucket
⏺ Bash(tigris objects put --help)
⎿ Usage: tigris objects put|p [options] [command] <bucket> <key> <file>
Upload an object
… +14 lines (ctrl+o to expand)
⏺ Bash(ls -la yeetfile.js 2>/dev/null || find . -name "yeetfile.js" -type f 2>/dev/null | head -5)
⎿ -rw-r--r-- 1 xe staff 5010 Jan 12 15:55 yeetfile.js
⏺ Bash(tigris objects put skynet yeetfile.js yeetfile.js)
⎿ ┌─────────────┬────────┬──────────────┬─────────────────────────┐
│ Path │ Size │ Content-Type │ Modified │
├─────────────┼────────┼──────────────┼─────────────────────────┤
… +2 lines (ctrl+o to expand)
⏺ Done. yeetfile.js has been uploaded to the xe-zohar-copy bucket (4.9 KB).
No hand-holding or custom skill files required! The agent reads the help text
and just figures it out. This is the kind of design we're aiming for: natural
enough that both humans and AIs can figure it out without needing to read a
tutorial beyond --help.
How did we get to this design?
Inside Tigris we use a Request-For-Comment flow for many of these big projects. A lot of the time in planning is spent drawing up a formal specification. We write this formal spec way before we even think about writing code. Having this all specified makes it a lot easier to iterate and gather feedback. Needless to say, this project generated a lot of feedback, probably the most out of any of these user-facing projects.
When Abdullah was working on the CLI, he wanted to write a formal specification for the CLI and then transform that into the scaffolding for the code. This eventually evolved into specs.yaml, which we parse with some custom code to generate the scaffolding required to wire everything up. This spec file ended up serving two purposes: it drove the RFC process by being the canonical source of truth for what the CLI should do, but it also was reused to generate the CLI itself. Change the spec, regenerate the CLI, everything updates and all is balanced as all things should be.
As a neat side effect of this, here's
the implementation of the objects put command.
It's in src/lib/objects/put.ts and directly maps to objects put. This gives
us filesystem-based command routing the same way that Next.js gives you
filesystem based web routing.
This spec-driven approach worked well enough that I think other teams should use it too. We're going to evolve this a bit more and make some tooling to generate CLI scaffolds in the future. Stay tuned!
Example commands
Here's some side by side examples comparing the tigris and aws commands for
interacting with object storage:
| Tigris Command | AWS S3 Equivalent | Description |
|---|---|---|
tigris ls | aws s3 ls | List buckets or objects |
tigris ls <bucket> | aws s3 ls s3://bucket/ | List objects in a bucket |
tigris ls <bucket>/path/ | aws s3 ls s3://bucket/path/ | List objects in a path |
tigris mk <bucket> | aws s3 mb s3://bucket | Create a new bucket |
tigris mk <bucket>/folder/ | aws s3api put-object --bucket bucket --key folder/ | Create a folder |
tigris touch <bucket>/key | aws s3api put-object --bucket bucket --key key | Create empty object |
tigris cp ./info.json t3://bucket/info.json | aws s3 cp ./info.json s3://bucket/info.json | Copy a file from the filesystem to Tigris |
tigris cp t3://bucket/info.json ./info.json | aws s3 cp s3://bucket/info.json ./info.json | Copy a file from Tigris to the filesystem |
tigris objects put <bucket> <key> <source> | aws s3 cp <src> s3://<dest> | Copy objects/folders |
tigris objects get <bucket> <key> > test.json | aws s3 cp file.txt s3://bucket/ | Upload file |
tigris rm <bucket>/key | aws s3 rm s3://bucket/key | Delete object |
tigris rm <bucket>/ | aws s3 rm s3://bucket/ --recursive | Delete folder recursively |
tigris configure | aws configure | Save credentials |
tigris whoami | aws sts get-caller-identity | Show current user info |
tigris logout | (no direct equivalent) | End session |
You can also use t3 as shorthand for tigris in the CLI!
See how much simpler that all is? We think that making these commands direct like this will make it a lot easier for you to understand and work with Tigris.
The real star: tigris cp
My favorite subcommand is cp (copy). It does everything you'd expect from the
UNIX tool cp on your local machine, but it understands t3:// URLs. This
means you can copy files between buckets, download stuff, upload stuff—all with
one command that behaves exactly like you'd expect:
# Copy a remote object to a new location
tigris cp t3://my-bucket/my-path/my-object.json t3://my-bucket/new-path/my-object.json
# Copy a remote folder recursively
tigris cp -r t3://my-bucket/my-path/ t3://my-bucket/new-path/
# Copy with wildcard
tigris cp t3://my-bucket/my-path/*.json t3://my-bucket/new-path/
# Download a remote object to local
tigris cp t3://my-bucket/my-file.txt ./local-file.txt
# Download a folder to local
tigris cp -r t3://my-bucket/my-path/ ./local-dir/
# Upload a local file to remote
tigris cp ./local-file.txt t3://my-bucket/my-file.txt
# Upload a local folder to remote
tigris cp -r ./local-dir/ t3://my-bucket/my-path/
See that t3:// prefix? That's our URL scheme for Tigris objects. Your brain
already knows how cp works; we just made it work with object storage too.
"Instead of being fixated on an existing tool, you can do something new" —Abdullah