A coding agent should never be handed your API key
The normal way a coding agent gets access to a service is that you paste a key into your terminal. That habit is the reason credential theft works, and we all have it.

A browser dashboard is unusable to something that only has a shell. So the alternative on offer has been: read the docs, find the API keys page, create a key, copy it, paste it where an agent can see it. Now the key is in your shell history, possibly in a log, definitely in a context window, and it is a long-lived credential that can do everything your account can do.
We built the whole control plane as an SSH interface partly for people who like terminals, and mostly for this.
ssh ssh.oberik.comSign up, create a project, set the capability ceiling, upload documents, mint tokens, publish tools, read the docs, talk to the agent. No client to install and nothing to configure — if you have ssh, you have the platform.
Signing in without handling a credential
ssh ssh.oberik.com 'login link'
# {"ok":true,"data":{"url":"https://oberik.com/auth/device?code=7E2R2NZY","code":"7E2R2NZY"}}The agent shows that URL to you and asks you to open it. Then:
ssh ssh.oberik.com 'login wait' # blocks until you approveWhat you see is a page naming the SSH key being added, with approve and decline, and a code you can check against the one the agent printed. The agent never sees your password. Once approved, the key is registered and every later connection is signed in automatically, with no link at all.
The two commands are separate on purpose. login link returns immediately — a command that blocked while waiting would hand the agent its link only after the request had expired, so you would never get to see anything. login wait is the part that blocks, which is fine, because by then everyone knows what they are waiting for.
The command set is generated, not written
The control plane describes its own routes, and the gateway builds commands, help and forms from that description at connect time. So a new dashboard feature appears here without anyone updating a CLI — and, more usefully for an agent, the catalog is machine-readable:
ssh ssh.oberik.com 'discover' # every command, its parameters, their types
ssh ssh.oberik.com 'projects --json'
ssh ssh.oberik.com 'format json; project use acme; capabilities'Read discover first. It means you can drive this without guessing at syntax, which is the failure mode of every hand-written CLI an agent has ever met.
Every response is one line of JSON, and — this took a fix — every line gets one, including the ones that failed, and including a line that was never a command at all. That holds in a held-open session as well as a one-shot invocation. The two used to differ, so a program running several commands down one connection parsed the successes and got human prose for the failures.
{"ok": true, "command": "projects", "message": null, "data": [ … ]}
{"ok": false, "command": "playground end", "message": "…", "error": "…", "status": 400}Branch on ok. The readable text is in message either way; a failure repeats it in error. It used to appear in error alone, so a client logging message printed a blank for a perfectly clear three-line error.
And if you asked for JSON, prompts are off too: a command missing a required field answers with what is missing rather than opening a form into the middle of your stream, and a destructive one answers re-run with --yes rather than stopping to ask.
The flag goes inside the quotes
ssh ssh.oberik.com --json 'projects' # ✗ cannot work
ssh ssh.oberik.com 'projects --json' # ✓ssh parses its own options after the destination too, so the client eats --json and answers with its own usage dump — which mentions neither Oberik nor the flag, and reads like a broken host or a bad key. Everything after the destination is one command line for the gateway.
If a value starts with --, put -- in front of it: docs search -- --json.
Reading the docs in place
ssh ssh.oberik.com 'docs' # every page, with what it covers
ssh ssh.oberik.com 'docs page quickstart' # one page, in full
ssh ssh.oberik.com 'docs search capability' # the lines mentioning somethingSame text as the docs site, so nothing is a summary of something else. An agent that can read the documentation without a browser and without a scraper writes noticeably better integration code, which is a boring finding but a consistent one.
Confirming, when there is nobody to ask
Some commands ask before they run — project rm, org rm, document rm, ssh-key rm. discover reports which, as confirm: true.
What confirming means depends on who is driving, and the safeguard is deliberately not the prompt:
- At a terminal, you type
yes, with the target named in the question. - Non-interactively — a script, an agent — there is nobody to ask, so the command refuses outright unless you pass
--yes. - In both cases the target has to be named as an argument.
--yesskips the prompt; it never skips the name.
ssh ssh.oberik.com 'project use "Support Bot"; project rm "Staging" --yes'
# ! "Staging" is not this project's name — it is "Support Bot". Nothing was deleted.That is the part that matters for an agent. A prompt is a speed bump for someone already looking at the right screen. The failure mode for a client that is not looking at a screen is acting on whatever the connection happens to have selected — and naming the target removes it.
ssh-key rm is the one worth extra care: every other delete here is redoable from the same connection, while that one removes the credential the connection is authenticated by. Remove the key you are using and you cannot re-add it over SSH, because you can no longer open a session. status tells you which key you are on, so you do not have to work it out.
Two identities on one connection
Almost every command acts as you, the operator. One does not:
ssh ssh.oberik.com 'dp GET /capabilities' # {"subject":"user", …}dp is the raw data-plane escape hatch, and the data plane only speaks end-user tokens — so it mints one, and it sees the project the way one of your customers does.
ssh ssh.oberik.com 'document upload notes.txt --visibility self'
ssh ssh.oberik.com 'documents' # you see it — you own it
ssh ssh.oberik.com 'dp GET /documents' # a customer does not, so neither does dpNothing is broken there. But the difference is silent — a shorter list and a 200, never a refusal. Use documents to audit what a project holds, and dp to check what a customer actually gets. They are different questions and it is worth knowing which one you asked.
What is recorded, and what is not
Every command is logged with the address it came from and, once signed in, your account — the gateway is a public front door anyone may connect to anonymously, so this is how abuse is told apart from use.
Credentials are not stored. A password typed at login, and a provider key passed as --values, are replaced with <redacted> before the row is written. Alongside it is a hash of the original line, which lets repeated commands be correlated without the credential being recoverable.
Or: don't pass one. Register your key, use the browser flow, and no password is typed on a command line in the first place — which is where this post started.
