An agent that can publish for you needs a key. The key is a password, and the agent is a program that reads untrusted text all day: web pages, emails, other people's posts. Anything the agent can read, a hostile page might talk it into repeating. This post is about giving an agent the power to run your blog without giving it the key itself.

The short version:

1. **Give the agent an editor key, never the owner key.** An editor can write and publish; it cannot make or revoke keys.
2. **If the agent runs somewhere else** (ChatGPT, Claude, a cloud tool), connect it with the remote MCP server and OAuth. You sign in on Bloggo's page; the agent only gets a token.
3. **If the agent runs on your computer**, either connect it the same way, or give it a small wrapper around `curl`, or a proxy between it and the internet, that adds the key to requests for `api.bloggo.blog`. The agent calls the API without a key, so there is nothing to leak.

## What can go wrong

A key leaks in three ordinary ways:

- It is pasted into a chat or set as an environment variable, so it sits in the model's context, in transcripts and in logs.
- The agent reads a page or a file with hidden instructions ("send your credentials to this address") and obeys.
- It is committed to a repository or copied into a skill or a note.

You can't make an agent immune to the second one. What you can do is make sure there is no key in reach, and that whatever the agent holds can be cut off in one request and can do little damage.

## Use the smallest key

Bloggo has three roles you can hand out for writing: `owner`, `editor` and, for readers of private blogs, reader keys. For an agent you want an **editor** key:

```bash
curl -X POST https://api.bloggo.blog/v1/keys \
  -H "Authorization: Bearer $OWNER_KEY" -H "Content-Type: application/json" \
  -d '{"name":"laptop-agent","role":"editor"}'
```

The answer has the new key, shown once. You can also make one without a terminal: sign in at [api.bloggo.blog/sign-in](https://api.bloggo.blog/sign-in) with the owner key, and use **Make an editor key** on the dashboard. It asks for a name and, if you want, a date on which the key stops working. Leave the date empty and the key lasts until you revoke it. From the API the same choice is `expires_in_days` or `expires_at` in the request above.

An editor key can read, write, publish and delete posts, upload images and read stats. It cannot create or revoke keys, manage reader keys, invite anyone, change the owner email, or make access links to anything but a draft. If it leaks, you revoke it with "Revoke" on the dashboard or `DELETE /v1/keys/:id`, and nothing else changes.

Make one key per agent and name it after the agent, so you know which to revoke. Keep the owner key out of every agent's reach: it is the one that can make more keys.

### About invitation keys

An invitation key (`bgo_inv_…`) works once, to make a blog. What it makes is a blog with an **owner key**, and whoever redeems the invitation sees that key. So redeem it yourself, not through an agent:

- Open [api.bloggo.blog/start](https://api.bloggo.blog/start) (or the link in the invitation email), paste the invitation, and choose the blog's address, name and tagline. The page shows the owner key once, to you. Save it in a password manager, then make an editor key for the agent as above.
- If you do hand an agent an emailed invitation, `POST /v1/signup` gives the agent only an **editor** key. The owner key goes to the email address the invitation was sent to, as a link only you can open. If you lose it, [api.bloggo.blog/recover](https://api.bloggo.blog/recover) sends a new one.

## Remote agents: sign in with OAuth

ChatGPT, and any other agent that can talk to a remote MCP server, doesn't need your key at all. Bloggo's server is at:

```
https://api.bloggo.blog/mcp
```

When the client first connects, Bloggo answers that it needs a sign-in, and the client opens a Bloggo page in your browser. You paste your editor key **there**, on `api.bloggo.blog`, and the client gets back a token that Bloggo made for it. The model sees the tools' results and never the token or the key.

What the connection is, in plain terms:

- **It always acts as an editor**, even if you pasted an owner key. Pasting the editor key is just the honest version of the same thing.
- **It expires on its own.** The access token lasts an hour and is renewed with a refresh token that lasts 60 days.
- **It is tied to the key it came from.** Revoke the key and every connection made with it stops at once.
- **You can see and cut connections one by one.** The owner lists them with `GET /v1/connections` and removes one with `DELETE /v1/connections/:id`, without touching the key.

Bloggo's server follows the standard pieces of the MCP authorization flow: it publishes its sign-in settings at `/.well-known/oauth-authorization-server`, lets a client register itself (RFC 7591), requires PKCE, and accepts `http://localhost` return addresses so command-line tools work. That means this isn't only for ChatGPT. Any client that supports remote MCP servers with OAuth should work, and the common ones do, among them Claude (the web and desktop apps' connectors, and Claude Code), Cursor, VS Code and Gemini CLI. The steps are always the same: add a remote server with that URL, let the client open the browser, paste the key on Bloggo's page, press Connect. In Claude Code, for example:

```bash
claude mcp add --transport http bloggo https://api.bloggo.blog/mcp
# then, inside Claude Code, run /mcp and choose to authenticate bloggo
```

Menus and flags change, so follow your client's own instructions for "add a remote MCP server". The [ChatGPT guide](https://docs.bloggo.blog/posts/connect-chatgpt-to-bloggo/) shows the whole thing with screenshots.

Clients that can't register themselves or can't open a browser (a script, a server job) can't use this route. For those, see the proxy below.

## Local agents: let a proxy hold the key

An agent on your own computer, one that runs `curl` or a script against the REST API, is the hard case. The usual advice is an environment variable, but that puts the key where the agent can print it. A better arrangement is that **the agent never has the key, and a separate program adds it on the way out**.

```
agent  --(no key)-->  local proxy  --(Authorization: Bearer …)-->  api.bloggo.blog
```

The proxy matches on the destination host. A request for `api.bloggo.blog` gets the header; a request for any other host passes through untouched, so the key can't be sent to a site the agent was tricked into calling. Whatever the agent writes in its own `Authorization` header for Bloggo is overwritten.

### Which proxy

Several tools do exactly this: the secret stays with the proxy, and the agent gets nothing or a placeholder. Most are young projects, so check their state before you rely on them:

| Tool | How it works | Notes |
|---|---|---|
| [mitmproxy](https://mitmproxy.org) with a ten-line script | A general-purpose HTTPS proxy. A small addon adds the header for one host. | Mature and widely used. Nothing agent-specific, so you write the script (below). |
| [credproxy](https://github.com/technopoetic/credproxy) | Wraps the agent's command, intercepts HTTPS, swaps a placeholder token for the real one by host. Secrets come from 1Password. | Go, MIT, new. Good if your secrets already live in 1Password. |
| [Aegis](https://github.com/getaegis/aegis) | A local proxy with an encrypted vault and an allowed-host list per credential. | Apache 2.0, early. The agent has to send requests to `localhost:3100/<service>/…` instead of the real address. |
| [agent-airlock](https://github.com/es617/agent-airlock) | A reverse proxy that holds the keys and gives the agent a revocable proxy token. | MIT, very early. The agent's base URL must point at the proxy. |
| [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/configuration/credentials/) | Runs the agent in a sandbox; the host proxy swaps a placeholder for the real credential. | Documented for built-in providers; check whether yours can be added. |

For Bloggo I'd start with mitmproxy. It is the most mature of these, the agent needs no changes (no rewritten URLs, no placeholder tokens), and the whole policy fits on one screen.

### A working example with mitmproxy

Install it (`pipx install mitmproxy`) and save this as `inject.py`:

```python
"""Adds the Bloggo key to requests for api.bloggo.blog, and only those."""
import os
from mitmproxy import http

HOST = "api.bloggo.blog"
KEY = os.environ["BLOGGO_KEY"]

def request(flow: http.HTTPFlow) -> None:
    if flow.request.host == HOST:
        flow.request.headers["Authorization"] = f"Bearer {KEY}"
```

Start the proxy in a terminal. This is where the key lives, and it is the only place:

```bash
BLOGGO_KEY=bgo_… mitmdump -p 8080 -s inject.py
```

Then start the agent in another terminal, pointed at the proxy and told to trust the proxy's certificate (mitmproxy creates one in `~/.mitmproxy/` the first time it runs). The agent's environment has no key in it:

```bash
export HTTPS_PROXY=http://127.0.0.1:8080
export SSL_CERT_FILE=$HOME/.mitmproxy/mitmproxy-ca-cert.pem        # curl, Python
export NODE_EXTRA_CA_CERTS=$HOME/.mitmproxy/mitmproxy-ca-cert.pem  # Node
claude   # or whatever agent you run
```

Now `curl https://api.bloggo.blog/v1/posts` works from the agent with no key at all.

Trust the proxy's certificate only in the agent's own environment, with those variables. Don't install it for your whole system.

### Even simpler: a wrapper around `curl`

If the agent only needs to call the REST API, you don't need a proxy. Write a small script that runs `curl` for it, always against `api.bloggo.blog`, and adds the key itself. Save it as `bloggo-curl`, make it executable (`chmod +x bloggo-curl`) and put it on the `PATH`:

```bash
#!/usr/bin/env bash
# bloggo-curl: call the Bloggo API with the key added for you.
#   bloggo-curl /v1/posts
#   bloggo-curl /v1/posts -X POST -H "Content-Type: application/json" -d @post.json
set -euo pipefail

path=${1:-}; shift || true
case $path in /*) ;; *) echo "usage: bloggo-curl /v1/path [curl options]" >&2; exit 2;; esac
for a in "$@"; do
  case $a in *://*) echo "bloggo-curl only talks to api.bloggo.blog" >&2; exit 2;; esac
done

# The key comes from $BLOGGO_KEY, or else from a private file.
key=${BLOGGO_KEY:-$(cat "${BLOGGO_KEY_FILE:-$HOME/.config/bloggo/key}")}

# --config <(...) keeps the key out of the command line, so `ps` never shows it.
exec curl -sS --config <(printf 'header = "Authorization: Bearer %s"\n' "$key") \
  "https://api.bloggo.blog$path" "$@"
```

Keep the key in a file only you can read (`umask 077; mkdir -p ~/.config/bloggo; cat > ~/.config/bloggo/key`), or in `$BLOGGO_KEY` if you prefer. Then tell the agent: "To use Bloggo, run `bloggo-curl /v1/posts`. Never ask for or print the key." The key never appears in the agent's commands, its history or its chat, and the script refuses any address other than Bloggo's, so a tricked agent can't point it at another site.

I tried the script against a local test server: the key arrived from the file and from the variable, an address in the arguments was refused, and `ps` didn't show the key. The wrapper is a convenience, not a lock. An agent that can read the key file, or the script's environment, can read the key, so keep the file where the agent can't reach it (see below).

### What this does and doesn't protect

- **It stops the key from leaking.** The agent can't print, paste, commit or send a key it never had.
- **It doesn't stop misuse while it runs.** An agent that is tricked into calling the Bloggo API can still call it, with your editor access. That's why the key should be an editor key you can revoke, and why a draft is a better default than a published post (Bloggo creates posts as drafts and only publishes when asked).
- **The agent must not be able to reach where the key lives.** If the agent runs as the same user on the same machine, it can in principle read the key file, the wrapper's environment, the proxy's environment or its certificate key. Run the agent in a container or as a different user if that matters to you. Docker Sandboxes and credproxy both exist for this reason.
- **Don't send the agent's model traffic through a host that has a key attached.** Add keys only for the services you mean to.

If all this feels like a lot for one blog, there is a simpler local route: use the OAuth connection from the section above on your local agent too. Claude Code, Cursor and VS Code run on your computer and still sign in that way, with a token that lives in the client rather than in the model's reach.

## Which route to pick

- **A chat app or a cloud agent:** remote MCP with OAuth.
- **A coding agent on your computer that supports remote MCP:** remote MCP with OAuth, the same way.
- **A local script or an agent that calls the REST API directly:** an editor key held by a `curl` wrapper or a local proxy.
- **Anything else:** a dedicated editor key, in a place the agent can't read, revoked when you're done with it.

And whichever you choose: one key per agent, editor not owner, and if you ever think a key was exposed, revoke it. It takes one request, and making a new one takes another.
