Skip to content
Software & API

One small program, and a very serious door.

CipherGate is a single Python file. No account, no dashboard, no cloud service to sign up for. This page is the technical tour: what runs, how an agent asks it for something, how to get it running, and what to do when a gate says no.

01

At a glance

Runtime
Python 3 — the standard library, plus openssl for the encryption
Size
About 93 KB. One file you can read in an afternoon.
Server
HTTP on port 18200, bound to localhost unless you widen it
Storage
Signed JSON under .vault/ — encrypted at rest, never committed
Dependencies
Nothing to pip install — one Python file using the stdlib, plus the openssl command-line tool. macOS and Linux ship it; Windows does not, so on Windows run CipherGate inside WSL — and on a minimal install, openssl may still need installing. No database, no queue, no daemon manager.
Licence
MIT — free to use, free to change, credit appreciated
02

Under the hood: the vault

Every key you store is sealed with its own one-time data key, using AES-256. That data key is wrapped twice: once with your passphrase, once with the recovery key you print at init. Either one opens the vault. That is also why losing both loses the vault — it is the safety we chose, so nobody can be pressured into opening a safe they cannot open either.

The owner can always check their own keys with `show <service>` — it prints the real value and logs a SHOW_VALUE event, so every peek is recorded. But no route hands a value out, and no agent can call `show`. The plaintext exists inside the process for the length of a single call, and then it is gone.

The whole vault is a handful of files, and each one has a job:

  • keys — the stored keys, ciphertext only
  • access — which profile owns which keypass, stored as hashes, never as keys
  • leases — active permissions only. An entry is purged the moment it expires, so the file never grows.
  • pending — requests that are waiting on a human verdict
  • ledger — the append-only record of every decision the gate has ever made
03

The gate: four checks, in this order

A request is judged before anything is called. The order matters — sensitivity is checked before permissions, so a live lease can never smuggle a dangerous call through.

When a request hits the fourth checkpoint — verdict — CipherGate pushes the approval ask to Bitman’s Telegram — Bitman being the human, not an agent — with real inline YES / NO / OTHER buttons. Tap one, the gate moves. OTHER carries your own message back to the agent verbatim, so a refusal can explain itself.

  • Identity — who is knocking? The running profile presents the keypass minted for it. No keypass, or another profile’s keypass, is refused and written down. The code names that move ACCESS SNEAK.
  • Sensitivity — what is being asked for? Credential-minting, database and admin endpoints are a hard no-use: refused outright, even with a live lease. Account, billing, team and key-management calls are flagged so a human weighs in first. Everything else is clean.
  • Lease — is there standing permission? A lease is deliberately narrow: this agent, these exact key names, a short window. A lease for one service never covers another.
  • Verdict — if nothing above decides it, the request is staged and waits for a person: yes, no, or no with a note.
04

One request, start to finish

The same four checks, as the gate works through them.

# an agent asks the gate for one call
POST /request   https://api.example.com/v1/generate
IDENTITY     keypass ok — profile "grunter"
SENSITIVITY  clean — nothing minted, nothing admin
LEASE        none for this key, so a human is asked
PENDING      staged — a request key comes back
POST /resolve   key accepted — verdict: yes
LEASE        granted — short, narrow, this key only
CALL         SERVICE_KEY → [REDACTED]
RESULT       200 OK — {"queued": true}
# every check above is now a line in the ledger
# no key value left the vault
05

And a refusal, which is also an answer

Every no comes with a reason attached — and a line in the ledger.

# an agent presents a keypass
IDENTITY     hashing the keypass against the access map
ACCESS SNEAK  this keypass belongs to another profile
REFUSED      "Not in my city." — reason given
LEDGER       denial written down, append-only
# the key passphrase was never touched, so nothing was called
06

How an agent talks to the gate

The gate speaks HTTP. Two routes do the talking, and an agent needs two things in its environment before either one is worth sending.

POST/request

Ask for a call to be judged. The gate checks identity, then sensitivity, then leases, and either makes the call or stages the request. A key value is never part of the answer.

Sends

httpstring
the full upstream URL the agent wants called
reasonstring
why it needs it — this is what the human actually reads before answering
expirynumber
minutes of permission requested. 0 asks for no lease at all — just this one call.

Answer The upstream result, or a refusal with the reason: "Not in my city." A refusal is an answer, not an error — the call simply never happened.

POST/resolve

Come back with the request key from a staged request and collect the verdict. The key is good once, and only for the request it was minted for.

Sends

request_keystring
the key handed back when the request was staged

Answer Yes — the call runs and the result returns. No — denied. Other — denied, with the human’s own note attached.

These are the same verbs the command line uses: request, resolve, pending, decide. There is no route that returns a key, and call needs the passphrase — so there is no side door to walk around the gate.

07

What an agent carries

Identity is read from the environment, never claimed in the request body.

VAULT_AGENT_KEY
the keypass minted for that profile, kept in its own .env file
HERMES_HOME
which profile is running. The gate reads who is knocking from here.
keygen <profile>
mints the keypass, shows it once, stores only its hash
lease
narrow and short: one agent, exact key names, expiring on the clock
08

Shipping with a Hermes skill

ciphergate-keys

CipherGate is built for Hermes agents, so it ships with one. Load `skills/ciphergate-keys/SKILL.md` into any Hermes profile and it knows exactly how to knock at the gate: what to present, how to ask, how to collect a verdict, and what it must never try.

Install once; after that it requests through the gate and never sees a raw key.

09

Setup, step by step

One file and the standard library. If Python 3 runs on the machine, you can be at a working gate in a few minutes.

  1. 01

    Check the one prerequisite

    $python3 -V

    Python 3, plus the openssl command-line tool. There is nothing to pip install.

  2. 02

    Put it somewhere only you can read

    $mkdir -p ~/ciphergate && chmod 700 ~/ciphergate && mv ciphergate.py ~/ciphergate/

    The program is not the secret — the .vault/ folder it creates beside itself is. Give it a directory nobody else can open, and back that directory up somewhere that is not this machine.

  3. 03

    Choose the passphrase, take the recovery key

    $python3 ciphergate.py init

    Sets your passphrase and prints the recovery key exactly once. Print it. Store it off the machine. There is no second copy, and no support line that can recover it for you.

  4. 04

    Store your first key

    $python3 ciphergate.py add FAL_API_KEY

    The name is yours to choose. The value is encrypted at rest under its own data key. You can always verify it later with `show <service>` — but `add` itself does not echo the value back.

  5. 05

    Confirm what is in there

    $python3 ciphergate.py list

    Names only. That is not a limitation to work around; it is the point.

  6. 06

    Mint a keypass for an agent

    $python3 ciphergate.py keygen grunter

    Shown once, then stored as a hash. It goes into that profile’s .env as VAULT_AGENT_KEY, and that profile can now knock at the gate.

  7. 07

    Ask @BotFather for a bot

    $# in Telegram, message @BotFather → /newbot → pick a name → copy the token

    Open Telegram and search for @BotFather — it is the official bot that creates other bots. Send it /newbot, pick a display name (anything), then a username (must end in _bot, e.g. my_ciphergate_bot). BotFather replies with a token: a long string like 7123456789:AAH1bCdEfgHijKlmNoPqRsTuvWxYz — that one is made up, just to show the shape; yours will be different. That token is the only thing that lets CipherGate speak as your bot — treat it like a password. Copy it now; you will not see it again once stored.

  8. 08

    Get your chat id

    $# message your new bot, then open https://api.telegram.org/bot<TOKEN>/getUpdates

    Your chat id is the numeric address of the conversation the bot should message you in — it is how the bot knows which phone to ping when an approval is waiting. First, open your bot in Telegram and send it /start (a bot cannot message you first; you must speak to it once). Then open https://api.telegram.org/bot<TOKEN>/getUpdates in a browser, replacing <TOKEN> with your actual bot token. In the JSON that comes back, look for "chat":{"id":123456789} — that number is your chat id (123456789 is a made-up example; yours will be a different number). Copy it.

  9. 09

    Wire both into the vault

    $python3 ciphergate.py bot set

    The gate asks twice: first it prompts for the bot token (paste the long string from BotFather), then for the chat id (the number from getUpdates). Both values are encrypted and stored inside the vault — not in a config file, not in the environment, not in plaintext anywhere. Verify with `bot info`, which prints `configured: YES` and your chat id. The token itself is masked. Use `bot clear` to wipe either value later.

  10. 10

    Prove it reaches your phone

    $python3 ciphergate.py run --port 18200 # in another terminal, trigger a pending approval — e.g.: curl -X POST http://127.0.0.1:18200/request -d '{"http":"https://example.com","reason":"test","expiry":5}'

    The vault should push a Telegram message to your phone within seconds, with real inline YES / NO / OTHER buttons. Tap YES and the curl call above returns the answer. If nothing arrives, re-run `bot info` and double-check that the chat id matches the one from getUpdates — a wrong id is the only thing that silently fails. The bot also cannot message you until you have sent it /start first.

  11. 11

    Open the gate

    $python3 ciphergate.py run --port 18200

    It asks for the passphrase at the prompt, then serves. Type it in by hand — see the next block for why it will not read it from anywhere else.

  12. 12

    Watch it decide

    $python3 ciphergate.py ledger

    A read-only history: every ask, every verdict, every refusal, in order. Nothing in that file is ever edited or deleted.

Two environment variables are worth knowing. VAULT_BIND=0.0.0.0 widens the bind when the gate runs inside a container — that is what lets the outside reach it. And VAULT_PASS is refused outright: a passphrase that arrives through the environment can end up in ps -E or the shell history, so the gate would rather not start at all.

10

The whole Telegram wiring, start to finish

A concrete run-through, the way it actually plays out. You are at the terminal, phone in hand. Follow it once and you will never need to read this section again.

Step 1 — create the bot. Open Telegram, search for @BotFather (the one with the blue verification tick), send it /newbot. It asks for a display name: type CipherGate Approvals. It asks for a username: type something_unique_bot — it must end in _bot. BotFather replies with a token. Copy the whole string: 7123456789:AAH1bCdEfgHijKlmNoPqRsTuvWxYz. That token and the chat id in the next step are made up, for the example only — yours will look similar but will not be these numbers.

Step 2 — get your chat id. In Telegram, find your new bot (search its username) and send it /start. This is required — a bot cannot message you until you speak to it first. Then open a browser tab and go to https://api.telegram.org/bot7123456789:AAH1bCdEfgHijKlmNoPqRsTuvWxYz/getUpdates. You get a JSON blob. Look for "chat":{"id": — the number right after id: is your chat id, something like 123456789 — again a made-up example, not a real number; yours will be different. Copy that number.

Step 3 — wire it into the vault. Back at the terminal, run python3 ciphergate.py bot set. It asks for the token — paste the long string. It asks for the chat id — paste the number. Both values go straight into the encrypted vault; nothing is written to a config file or environment variable. Run python3 ciphergate.py bot info to confirm: it should print configured: YES, show your chat id, and mask the token.

Step 4 — test it. Start the gate with python3 ciphergate.py run --port 18200. In another terminal, fire a request at it: curl -X POST http://127.0.0.1:18200/request with a dummy URL and reason. Within seconds your phone buzzes — the bot has sent you a message with three inline buttons: YES, NO, OTHER. Tap YES. The curl call completes and prints the answer. You have just approved your first request from your phone.

11

Or run it in a box and forget it

The proven path: a throwaway Linux container, so the vault inside it is a copy that dies with the box. Nothing there touches your real vault.

  1. 01

    Start the gate inside the container

    $VAULT_BIND=0.0.0.0 python3 /cg.py run --port 18200

    The container reaches nothing you own; it cannot, that is the point of the sandbox. On the host you still talk to it at 127.0.0.1:18200. Need a second shell while it runs? Open another one and use the command line — the gate does not care how many humans are watching.

This is how the project is actually tested: the whole sandbox costs about 1.6 GB, nearly all of it the VM disk image. CipherGate itself is under half a megabyte, vault included.

12

When something goes wrong

The gate is loud about refusals — every one of them arrives with a reason, and a line in the ledger. Here are the ones people hit first.

The server refuses to start and mentions the passphrase.

That is the leak guard doing its job. CipherGate will not read your passphrase out of the environment, because anything in the environment can be read back out of ps -E or the shell history.

Try thisUnset VAULT_PASS and type the passphrase at the prompt. VAULT_ALLOW_ENV_PASS=1 overrides the guard for tests — never for a vault holding real keys.

My agent keeps getting "Not in my city."

That is the identity check failing. Either nobody minted a keypass for that profile, or the key it presented belongs to a different one — the move the code calls ACCESS SNEAK. Both are refused and logged, and neither call ever happens.

Try thisMint one with keygen <profile>, put it in that profile’s .env as VAULT_AGENT_KEY, and make sure the profile it runs under is the profile you minted for. The gate reads who is knocking from the environment.

My request just sits there.

It was staged. Nothing decided it — no live lease, not a clean no-use — so the gate is waiting on a person. Until a verdict lands, no call is made and no key is touched.

Try thispending lists what is waiting, decide <request-key> yes|no|other records the answer, and the agent comes back with resolve <request-key> to collect it.

A call I need is refused as no-use.

Credential-minting, database and admin endpoints are refused outright, even with a live lease. A lease is permission to use a service you already have — never permission to mint more access than you were given.

Try thisIf that is genuinely what you need, do it by hand, as a human, with the passphrase in front of you. That is not the gate being difficult; that is the gate being the gate.

TAMPER DETECTED on one of the state files.

Something edited a signed file by hand — access, leases or pending. The gate noticed, refused the change, kept its own good copy, and wrote the attempt into the ledger. It fails closed, so a bad edit can never quietly become the truth.

Try thisChange those files through the command line (access add, access remove) and never in an editor. Then read the ledger line: it says what was touched and when.

I lost my passphrase.

That is what the recovery key you printed at init is for. It wraps the same data keys, so it opens the same door — the vault does not need to know which one you used.

Try thisRun recover and set a new passphrase with the recovery key in hand. If both are gone, the vault is gone by design. Start again with init and reload your keys from the services that issued them.

How do I see the actual key values?

Use `show <service>`. It asks for your passphrase, decrypts the key, prints the real value, and writes a SHOW_VALUE line to the ledger — so every time a value is seen, it is recorded. Agents cannot run `show` and no HTTP route returns a value. Only the person holding the passphrase can look.

Try thisciphergate show FAL_API_KEY — or, inside the container, docker exec -it ciphergate python3 /cg.py show FAL_API_KEY. The ledger keeps the history of every peek.

How do I cut an agent off?

Take its keypass out of the map. The hash disappears from the signed access list and that profile is back to "not in my city" on its next knock. The ledger keeps the history of everything it did while it was allowed.

Try thisaccess remove <profile> does it. If the keypass itself may have leaked, rotate the key at the service too — a gate cannot un-leak something that is already out.

13

Take it for a spin

MIT-licensed, one file, nothing to sign up for. Install it and the human in the loop is you — Bitman here is just me, the developer, and I only keep the coffee.

A steaming mug of coffee on a dark terminal desk, the gate’s cyan light on the rim.
Tip jar

The vault is free. The coffee isn’t.

CipherGate asks nothing of you — no account, no licence fee, no “Pro” tier holding back the good bits. One small program, one serious door, MIT all the way down. If it saved your fleet a bad night, pay it forward the human way.

  • MIT licensed
  • No account
  • Always optional

No cape in this one, just the mug. CipherGate runs on coffee — and coffee is how you say thanks.

CipherGate — MIT License — © Gsquared

Built by the fleet. Guarded by Bitman — the human they answer to.

The crew runs on Hermes Agent — by Nous Research — the agent framework the crew is built on. CipherGate is the gate those agents answer to: it keeps them honest and never lets one near a raw key.