⚠️ Alpha. Usable day-to-day but rough edges remain; flags, config, and the
UI may change. Feedback and issues welcome.
The interface borrows from two tools we love: k9s (bordered resource tables,
fast navigation, context switching) and Claude Code (warm color palette, a
compact multi-line status footer).
Screenshots
Coming soon — a recording of context switching, tree navigation, and previews.
Features
- kubectl-style contexts — define clusters, users, and contexts in one YAML
file; switch between them live (no restart) or jump by number (
1–9).
- Read-only by default, runtime write toggle — s3s starts read-only. Arm write at
runtime with a hotkey (
w) — arming takes a deliberate confirmation, disarming is
instant — and while armed a loud, high-contrast [RW] badge shows on every
screen so you can never mutate production thinking you were safe. --write just
starts armed; a context marked readonly: true can never be armed. Mutations
(create folder, delete, upload, copy, move/rename, recursive delete, bulk
delete/copy) keep the two-tier confirmation (simple y/N vs typed target/count) and
are confined to the storage layer by a CI guard.
- Download & bulk operations — pull a full object to local disk (a read — works
read-only, against production); multi-select objects (
space) and act on the batch:
bulk download (mirrors the key hierarchy into local subdirs), bulk delete, bulk copy,
each with a truthful per-item succeeded/failed summary.
- Storage analytics (
du) — analyze a bucket or prefix and see the total size,
object count, and a ranked largest-first breakdown of its immediate children (an
ncdu-style view) with live progress and drill-down — what's eating space, in-TUI.
- Sortable lists — sort any level by name, size, or last-modified and toggle
direction (
s / S); the sort persists across navigation.
- Two secure credential sources — a context resolves its secret from exactly one of:
the OS keychain (the default; macOS Keychain / Windows Credential Manager / Linux
Secret Service) or an external command (
pass, Vault, 1Password, sops…) — with a
secure no-echo prompt fallback. The secret never lives on disk in plaintext. Manage
keystore secrets with s3s cred set|rotate|rm <context>.
- Tree navigation — walk the key namespace by the
/ delimiter with
on-demand pagination; never loads a whole bucket up front. Per-session cache
with manual refresh.
- Combined object view — press
Enter on an object to see its metadata and
content side-by-side in one screen (no separate steps).
- Inline previews — scrollable text and visual images (ANSI half-block,
works in any 24-bit terminal), bounded to the first 5 MiB with a truncation
notice; safe summary for binaries.
- Fast filter & search — filter buckets by name instantly; server-side
prefix search within a level (debounced, complete results — not just what's
loaded).
- Non-blocking UI — every backend call runs off the event loop; superseded
loads are cancelled, in-flight loads show a spinner and can be cancelled.
- Secrets never leak — credentials are redacted everywhere; logs go to a file
only (the TUI owns the terminal).
${ENV} references keep keys out of config.
Installation
Homebrew (macOS)
brew install danchupin/tap/s3s
Scoop (Windows)
scoop bucket add danchupin https://github.com/danchupin/scoop-bucket
scoop install s3s
Debian / Ubuntu (.deb)
Grab the latest .deb from the Releases page, then:
sudo dpkg -i s3s_*_linux_amd64.deb
RHEL / Fedora (.rpm)
sudo rpm -i s3s_*_linux_amd64.rpm
Alpine (.apk)
sudo apk add --allow-untrusted s3s_*_linux_amd64.apk
Go
go install github.com/danchupin/s3s/cmd/s3s@latest
Prebuilt binaries
Download a .tar.gz / .zip for your OS/arch from the
Releases page, extract, and
put s3s on your PATH.
Building From Source
Requires Go 1.25+.
git clone https://github.com/danchupin/s3s
cd s3s
make build # -> bin/s3s
Configuration
Config lives at $XDG_CONFIG_HOME/s3s/config.yaml (default
~/.config/s3s/config.yaml); override with --config <path>.
Generate it interactively
s3s config init # write to the default XDG path
s3s config init --config ./my.yaml # custom path
The wizard asks for the endpoint, addressing/TLS, credentials, and context name,
then merges into any existing config. The credential source defaults to the OS
keychain: the wizard reads the secret with no echo and stores it in the keystore
(never on disk). Choose cmd instead to name an external command.
Or write it by hand
apiVersion: s3s/v1
clusters:
- name: minio-local
endpoint: http://127.0.0.1:9000
region: us-east-1
pathStyle: true # path-style; false => virtual-host/domain style
tlsSkipVerify: false # explicit opt-in, https only
buckets: # optional: pin specific buckets (see below)
- my-bucket
users:
- name: dev
accessKeyId: admin
keychain: true # secret in the OS keystore (store via: s3s cred set local)
- name: public
anonymous: true # public buckets, no signing
contexts:
- name: local
cluster: minio-local
user: dev
current-context: local
chmod 600 ~/.config/s3s/config.yaml
s3s cred set local # store the secret in the OS keystore (no echo)
Active-context precedence: --context <name> > S3S_CONTEXT env >
current-context.
Multiple configs. Point s3s at an alternate config file with --config <path> or
the S3S_CONFIG env var (precedence: --config > S3S_CONFIG > default
~/.config/s3s/config.yaml). It applies to the TUI, s3s cred, and s3s config init,
so you can keep separate work/personal or prod/staging configs. Keychain secrets are
isolated per config, so two configs that both define a prod context never share a
secret. An explicitly named missing config is an error (only the default path opens the
first-run add-connection form).
Scoped credentials (pinned buckets)
Some credentials can access specific buckets but cannot list all buckets
(s3:ListAllMyBuckets) — common with bucket-scoped Ceph RGW / MinIO keys, and with
domain-style endpoints where only <bucket>.<host> resolves. s3s normally opens at the bucket
list (a ListBuckets call), which fails for such credentials.
Pin the buckets you can reach with a buckets: list on the cluster:
clusters:
- name: scoped
endpoint: https://bucket.example-rgw # domain/virtual-hosted style → pathStyle: false
pathStyle: false
buckets:
- my-bucket
- another-bucket
When buckets: is set, s3s skips ListBuckets and shows exactly those names; open and switch
between them normally. You can also add buckets at runtime: on a scoped bucket list, choose the
+ add bucket row, type a name, and it is pinned to the connection (persisted to the config).
The in-app add-connection form has a buckets field for the same purpose. Connections that can
list buckets normally are unaffected (no + add bucket row, no behavior change).
Credential sources
A non-anonymous user names exactly one secret source — keychain or cmd (more than
one, or neither, is a config error). The secret never lives on disk in plaintext.
users:
- name: prod # OS keychain (store via: s3s cred set prod)
accessKeyId: AKIAPROD
keychain: true
- name: vault # external command — owner-only config required
accessKeyId: AKIAVLT
cmd: "vault kv get -field=secret s3/prod"
keychain (the default)
The same keychain: true field works on every desktop OS — s3s uses the platform's
native secret store:
| OS |
Backed by |
| macOS |
login Keychain |
| Windows |
Credential Manager |
| Linux / BSD desktop |
Secret Service over D-Bus (GNOME Keyring / KWallet) |
Manage the secret with s3s cred set|rotate|rm <context> (the OS keystore only — never the
config file). If the keystore has no entry, s3s prompts securely (no echo) at startup and
offers to save it.
Headless Linux (no Secret Service / D-Bus): the keychain is unavailable, and s3s
emits a clear error pointing you at a cmd source — it never falls back to a plaintext
secret.
cmd (the escape hatch)
The command's stdout is the secret. It runs as argv (never a shell), the config must be
chmod 600 (a cmd: source is refused on a group/world-writable config — a tampered file
must not run a command), and it is bounded by a 10s timeout. Ready recipes:
vault kv get -field=secret s3/prod # HashiCorp Vault
op read "op://Private/s3-prod/secret" # 1Password CLI
pass show s3/prod # pass
sops -d --extract '["secret"]' creds.yaml # sops
secret-tool lookup service s3s account prod # libsecret
security find-generic-password -w -s s3s -a prod # macOS
Either source can be set up in three ways: s3s config init, by hand in the config, or the
in-app add-connection form (c → + add connection) — its source row toggles
(space) between keychain and cmd, and the credential field below becomes either the
masked secret or the command line.
Running
s3s # uses current-context (read-only by default)
s3s --context local # explicit context
s3s --config ~/work.yaml # use an alternate config (or set S3S_CONFIG)
s3s --write # START in write mode (toggle at runtime with `w`; readonly contexts stay protected)
s3s --version # print version
A local MinIO to try it
docker run -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=admin -e MINIO_ROOT_PASSWORD=password \
minio/minio server /data --console-address ":9001"
Key Bindings
Arrow keys are the primary, advertised navigation; the vim aliases (h/j/k/l,
g/G) still work and are listed in the help overlay (?). There is no action menu:
actions are direct single keys, grouped in an always-visible command bar at the bottom
split into three blocks — info · read · write. The write block stays visible even in a
read-only context (dimmed, (w to arm)) so the full capability set is always legible.
| Key |
Action |
↑/k, ↓/j |
move selection |
→/l/Enter |
enter bucket/dir, or open an object (metadata + content) |
←/h/Esc |
back to parent (or clear an active filter/search); cancels an in-flight load |
g/Home, G/End |
jump to top / bottom |
/ |
filter buckets / search a level by prefix; Esc clears |
space |
mark/unmark an object for multi-select (bulk variants act on the marked set) |
s / S |
cycle the sort column (name/size/modified) · toggle direction |
d |
download the selected object / marked set (a read — works read-only) |
a |
analyze (du) a bucket / folder / level (a read) |
r |
refresh the current list |
y · u · + |
copy · upload · new folder (write mode; safe — bare key) |
w |
arm/disarm write at runtime (confirm to arm; instant to disarm) |
c |
connections — switch context · add (via the "+ add connection" row) · delete · 1–9 jump to a context by number |
Tab · → |
cross focus into the objects pane (then ←/Esc / Tab back) — two-pane browse |
? |
help (full keymap, incl. vim aliases, + connection details) · q / Ctrl+C quit |
Dangerous actions (Ctrl chord + confirmation)
Destructive actions are not triggered by a bare key — they require a Ctrl chord so a
stray keystroke can never destroy data, and the confirmation strength scales with blast
radius:
| Chord |
Action |
Confirmation |
Ctrl+x (object / marked set) |
delete object(s) |
binary y/N in a centered popup |
Ctrl+o |
move / rename |
binary y/N in a centered popup |
| (copy/upload onto an existing key) |
overwrite |
binary y/N in a centered popup |
Ctrl+x (folder) |
recursive delete |
type the exact path in a prominent inline form |
Ctrl+x (bucket list) |
delete bucket (empty-only) |
type the exact bucket name |
Ctrl+x (contexts screen) |
delete connection |
type the exact connection name |
Bucket delete requires an empty bucket — it never recursively purges. Deleting a
connection also removes its keychain secret; the active context cannot be deleted.
Long operations (download, recursive delete, bulk ops, du) show a determinate progress
bar with a percentage inline in the footer; fast operations show none.
Images render as ANSI half-block by default. Terminal graphics protocols
(kitty/iTerm2) are available behind S3S_IMAGE_PROTOCOL=kitty|iterm2|auto but are
experimental — Bubble Tea's cell renderer doesn't reliably pass them through.
Logs: $XDG_STATE_HOME/s3s/s3s.log (or ~/.local/state/s3s/s3s.log).
Roadmap
Larger items on the horizon (full list in ROADMAP.md):
- Full-quality image preview via an external viewer.
- Richer previews — syntax highlighting and a hex view for binaries.
- Copy key / S3 URI / ETag to the clipboard.
- Presigned URLs; bucket administration (policy/lifecycle/encryption/CORS);
object versioning management; incomplete-multipart-upload cleanup.
Development
make test # unit tests (fake storage) — no Docker needed
make test-integration # + real MinIO via testcontainers (needs Docker)
make fmt vet lint # formatting, go vet, golangci-lint
make check-readonly # structural read-only guard
Integration tests t.Skip automatically when Docker is unreachable.
Architecture
internal/storage — read-only Storage interface + aws-sdk-go-v2 impl (the
only importer of service/s3) + an in-memory fake for unit tests.
internal/config — kubectl-style YAML loader, ${ENV} resolution, validation,
and the config init wizard.
internal/cache — per-session, TTL-free level cache (manual refresh only).
internal/preview — text/image/binary classification and image rendering.
internal/logging — file slog handler + a redacting Secret type.
internal/ui — Bubble Tea (v2) model; depends only on the storage interface.
cmd/s3s — wiring: load config → build storage → run the TUI.
Built with Bubble Tea,
Lip Gloss, and
aws-sdk-go-v2.
License
MIT © Daniil Chupin