One command. No database, no Go, no Node, no configuration.
On Linux and macOS:
curl -fsSL https://raw.githubusercontent.com/apache/casbin-gateway/master/scripts/install.sh | bash
On Windows, in PowerShell:
irm https://raw.githubusercontent.com/apache/casbin-gateway/master/scripts/install.ps1 | iex
Either one downloads the build for this machine, unpacks it into ~/.local/share/casbin-gateway (%LOCALAPPDATA%\casbin-gateway on Windows), puts a casbin-gateway command on your PATH, starts it, and arranges for it to start again when you log in. The terminal you installed from is yours again straight away.
Gateway then opens in its own window — no sign-in: it serves this machine only and signs the local admin in on sight. Closing that window leaves Gateway running behind its tray icon, which is also where you reopen the window, turn Start at Login off and on — Settings → Startup is the same switch — and quit for real. There is a Casbin Gateway entry on your desktop and in the Start menu, in ~/Applications, or in the application menu, depending on the platform. An archive unpacked by hand gets the same entry the first time the launcher runs.
If you would rather use a browser, or you are on a machine with no desktop at all, everything is still at http://localhost:17000, and casbin-gateway start runs the server on its own with no window and no tray.
That is the whole installation. Gateway keeps its data in a SQLite file inside its own directory.
The password behind that account is admin / 123, and it only matters if you open Gateway to the network — see Serving other machines.
| Agent | Monitoring | Provider | MCP | Skills | Prompt | Sessions | Install |
|---|---|---|---|---|---|---|---|
| Claude Code | ✅ | ✅ Anthropic | ✅ | ✅ | ✅ | ✅ | npm · brew · winget · script |
| Claude Desktop | ✅ | — | ✅ | — | — | ✅ | winget |
| Codex CLI | ✅ | ✅ OpenAI | ✅ | ✅ | ✅ | ✅ | npm · brew · winget · self |
| ChatGPT Desktop (Codex) | ✅ | ✅ OpenAI | ✅ | ✅ | ✅ | ✅ | store |
| Gemini CLI | ✅ | ✅ Gemini | ✅ | ✅ | ✅ | ✅ | npm |
| Cursor | ✅ | — | ✅ | ✅ | — | ✅ | brew · winget |
| Cursor Agent | ✅ | — | ✅ | ✅ | — | — | script · self |
| Windsurf | ✅ | — | ✅ | — | ✅ | — | brew · winget |
| Cline | — | ✅ OpenAI | ✅ | ✅ | — | — | npm |
| Qwen Code | ✅ | ✅ OpenAI | ✅ | ✅ | ✅ | ✅ | npm |
| iFlow CLI | — | ✅ OpenAI | ✅ | — | ✅ | — | npm |
| Kimi Code CLI | — | ✅ OpenAI | ✅ | ✅ | ✅ | — | npm · winget |
| CodeBuddy Code | — | ✅ OpenAI | ✅ | ✅ | ✅ | — | npm |
| Roo Code | — | — | — | — | — | — | — |
| Copilot CLI | — | — | — | — | — | — | npm |
| Continue | — | ✅ OpenAI | — | — | — | — | npm |
| Zed | — | ✅ OpenAI | — | — | — | — | brew · winget |
| Aider | — | ✅ OpenAI | — | — | — | — | — |
| goose | — | ✅ OpenAI | — | — | — | — | brew |
| Crush | — | — | — | — | — | — | npm · winget |
| Droid | — | ✅ OpenAI | — | — | — | — | — |
| Trae | — | — | — | — | — | — | brew · winget |
| opencode | ✅ | ✅ OpenAI | ✅ | ✅ | ✅ | ✅ | npm · winget · self |
| opencode Desktop | ✅ | ✅ OpenAI | ✅ | ✅ | ✅ | ✅ | winget |
| OpenAgent | ✅ | — | — | — | — | — | — |
| OpenClaw | ✅ | ✅ OpenAI | ✅ | ✅ | ✅ | ✅ | npm |
| Hermes Agent | ✅ | ✅ OpenAI | — | — | — | — | self |
| DeepSeek Harness | ✅ | ✅ OpenAI | ✅ | ✅ | — | ✅ | npm |
| Pi | — | ✅ OpenAI | — | ✅ | ✅ | — | npm |
The version Gateway is running sits in the top-right corner, with the date it was built, and it says New when the published build is a later one. Open it and press Update now: Gateway downloads the build for this machine, checks that it runs, puts it in place of itself and restarts into it. The page reloads on the new version when it comes back, and nothing else has to be touched — the data, the settings and the casbin-gateway command all stay where they are.
Gateway installed some other way, or in a directory it cannot write to, says so and shows the install command to run by hand instead.
| Page | What you get | What it needs |
|---|---|---|
| Agents | Every AI coding agent installed on this machine — Claude Code, Codex CLI, Cursor, the Gemini CLI, opencode and more — four cards to a row, each naming the account it is signed in to, the provider it answers to, what it has spent — day by day over the last month, wherever its own transcripts carry that much — and whether it is running right now. Start or stop one from its card, or run several instances of the same agent at once, each with a state directory and an account of its own. An agent this machine does not have is listed too, and installed or upgraded from the page through the package manager the host already has. Gateway reports which installations have a newer release waiting, moves one onto any version its manager still publishes - back to an older one when an update broke something - and uninstalls it, leaving the agent's own sign-in and history where they are. An agent unpacked by hand is found on PATH as well as in the layouts installers write, and one hiding somewhere neither describes is pointed at directly: Locate browses this machine and lists the program you pick as an installation of its own, without moving or changing anything. | Nothing |
| Agent versions | Every agent Gateway knows in one table: the build on this machine, the release its package manager publishes, and whether the two differ. Install one, upgrade it, put it back on an older release, or remove it, from the row it is on - in one click, whichever way it was installed: a package manager, the Microsoft Store, the uninstaller the app registered with Windows, the agent‘s own updater, or the vendor’s own install command. The command is shown before it runs, the row carries its progress while it does, and everything it printed is a click away. | Nothing |
| Skills, MCP & Prompts | Every skill, MCP server and instruction file of every agent in one table. Install skills from a GitHub repository, a .zip or .tar.gz, or a folder on this machine, into one agent or several at once. Add an MCP server the same way, edit the instructions an agent reads before every session, open one, delete it, or copy it into another agent. | Nothing |
| Sessions | Every session those agents have had, read from the transcripts they leave on disk: the whole conversation, message by message. How many there are, how many ran today and how many came from a transcript rather than from monitoring, then filtered down to one agent or one of those two sources. | Nothing |
| Activity | What a monitored agent is doing as it does it — each tool call, its target and how long it took. | Monitoring on for an agent |
| Providers | One endpoint in front of your model vendors. Gateway holds the API key, so the agents never have it — or forwards the agent's own sign-in and holds nothing. | A vendor API key, or nothing at all |
| Authenticity | A score out of 100 and a grade for every provider, measured without being asked — see the section below. | A provider with an API key |
| LLM Records | Every request an agent relayed: the full system prompt, every message and tool call, the schema of every tool the model was offered, plus tokens and cost. | A provider, and llmRecordMode — see Recording prompts |
| Usage | What every agent on this machine spent, over time and broken down by model and by agent, read from the agents' own transcripts — so it counts the requests that never went through Gateway. A second tab shows what Gateway relayed, which is the only account that knows which provider answered and whether it failed. | Nothing |
| Model pricing | What a million tokens costs, which is what every figure on the Usage page is worked out from. Edit a price by hand, or let Gateway reprice the models this machine has run from the models.dev catalogue on a schedule; a price you edited yourself is left alone. | Nothing |
A page with tabs or sections is in the sidebar under its own name, so the rail lands on the test cases, the MCP servers or the security settings rather than only on the top of the page they are on. ⌘K, or Ctrl+K, opens a search box over whichever page you are on: every page by name, and every agent and provider on this machine, which is how you reach one of thirty providers without paging through the list. The header switches the UI between light and dark, between three palettes — Amber & Ink, Terminal and Indigo — and between English and Chinese.
Agents are found by reading the user accounts, home directories and install paths of the machine Gateway runs on, so run it on the machine whose agents you want to watch.
A reseller can sell a frontier model and serve a cheaper one, count a cached prefix as fresh input, or answer in an API it only pretends to speak. None of that shows up in the traffic, so Authenticity asks the upstream directly. Every provider is probed on its own — when it is added, when its endpoint, type or key changes, when it has never been probed, and again whenever its report goes stale — and comes back with a score out of 100 and a grade from A to F, on the Authenticity page and above the agents on the home page. No button to press, and nothing to configure.
The score is only a summary of the test cases behind it, and every one of them is on the page. Half of them ask what the upstream is: whether the model that answers is the one that was asked for — worth half credit off a vendor‘s own endpoint, where that field is whatever the upstream typed there — which vendor the model says trained it, whether anything was injected in front of the request, whether several identical requests come back from the same model at all, and whether a parameter the API documents (logprobs, n, a stop sequence) is honoured, refused, or accepted with a 200 and quietly dropped. A test bank asks questions with one right answer, from counting the letters in a word to who wrote the Preface to the Pavilion of Prince Teng. The rest read the envelope: whether a two-level tool schema survives a forced call, whether the event stream carries everything the API documents, whether the prompt cache is really accounted for, whether two identical requests are billed the same, whether the vendor’s own headers are there. Each case names the question it puts to the upstream, the exact request it sends, how the answer is judged, and what it is worth.
Reweight a case, turn it off, rewrite its question, or add one of your own — the questions worth asking of a reseller are not the same everywhere, and a score whose method is not published is not evidence. Restore defaults puts the shipped suite back and leaves your own cases alone.
The report has a second half that costs nothing and sends no request: what the records Gateway already kept say about that upstream — how much of the cache it really accounted for, how many attempts failed, how long it took to answer, and how much of what it served has no price.
A probe spends a few cents of that provider's own credit, which is on the report next to the finding. providerProbeIntervalHours sets how often a report goes stale, providerProbeMode = "manual" probes only when asked, and "off" never probes.
This is what fills LLM Records, and what lets Gateway keep the vendor key instead of the agent.
export ANTHROPIC_BASE_URL="http://localhost:17000/v1/agents/claude-code" export ANTHROPIC_AUTH_TOKEN="cg-..."
The token is Gateway‘s own relay token, not a vendor key: the agent refuses to start without something in that variable, and Gateway authenticates upstream with the provider’s key instead. The snippet on the page already has the real value filled in.
One base URL answers whichever API the agent speaks: /chat/completions for an OpenAI client, /v1/messages for an Anthropic one, /responses for Codex, which speaks nothing else since it dropped the chat completions wire format, and /v1beta/models/<model>:generateContent for the Gemini CLI, which speaks only Google's own API. The API the provider serves need not be the same one: Gateway translates between all four, in both directions and for streamed answers too, so Codex runs on DeepSeek, Kimi or Qwen, and Claude Code runs on any of them just as well. A provider serving the very API the request arrived in is relayed byte for byte, untouched, since there is nothing to translate. The one thing Gateway answers itself is the token count an Anthropic or Gemini client asks for before each turn, which it estimates when the bound provider has no endpoint to ask.
Upstream, which API Gateway calls is the provider's own setting: Upstream API defaults to the one the provider type implies, and can be set to Responses for OpenAI itself or for a relay that accounts cached input on /responses more accurately than on /chat/completions. A Codex request then meets a Responses provider on both sides and passes through byte for byte, cache counters included.
An agent signed in with a ChatGPT or Claude subscription has no API key to paste. Set the provider‘s Authentication to **the caller’s own login** and it needs none: the base URL points at the vendor, and every request is forwarded with the credentials the agent itself sent, so it keeps its own sign-in. Leave Models empty and the provider accepts any model name.
The environment snippet for such a provider sets the base URL and nothing else — a token there would replace the sign-in the agent already has. Gateway records and routes the traffic exactly as it does for a provider with a key; it just never sees one.
Codex is the exception: its ChatGPT sign-in talks to an endpoint no provider stands in for, so a Codex CLI still needs a provider with an API key.
Vendors put an “add this to my agent manager” button on their own pages, which opens a ccswitch:// link rather than a page. Gateway reads that format instead of inventing a second one nobody would be given a button for, and one link carries any of four things: a provider with its base URL, key and model list; an MCP server, as the JSON block a server is written in; a set of instructions for an agent to read before every session; or a repository to install skills from.
Gateway claims the scheme on Windows and Linux when it starts, so clicking such a button opens Gateway on the Import page with everything the link held laid out — the arguments an MCP server would be run with, the instructions in full, the repository a skill would come from. Nothing is written until the button under it is pressed: the link came from a website, so its values are read first. A provider goes on to the Providers form, where it is reviewed and probed against the upstream before it is stored; the other three are written through the same endpoints that add one by hand, into the agents you tick. A link names apps rather than the agents on this machine, and the ones Gateway does not manage are named back to you rather than quietly dropped.
The link is handed to Gateway in the body of an API call, not in the address of the page it opens, because a provider link carries an API key and an address is kept in browser history and sent on to wherever the page navigates next.
The scheme is taken from whatever held it only the first time, and given back when Gateway is removed; a registration that is already Gateway's own is rewritten on every start, since it records a path that an update or a move would otherwise leave pointing at a Gateway that is no longer there. On macOS nothing is claimed at all — a URL scheme there belongs to an application bundle and the link arrives as an Apple event, which this launcher has no loop to receive, so declaring it would take links away from whatever can open them. Paste the link into the box on the Import page instead, which works on every platform; the Providers page takes a provider link the same way.
Permissions in the sidebar is the page for it: every agent on this machine down one side, what the one you picked may do beside it. The same card is on the agent's own page. Everything that agent relays through Gateway is held to what is set there, so it can be given less than it came with without editing its own configuration.
Bash, shell and run_shell_command are all the same switch. Each group ends in a catch-all for the tools Gateway has never seen, which is what closes a group for good rather than for the tools that happened to be listed the day it was set.*, so claude-opus-* covers a whole family.A request that asks for something switched off comes back as a permission_error in the API the agent speaks, so it reads as a refusal rather than as a broken gateway.
Underneath, the switches compile to a Casbin policy, and every relayed request is decided by an enforcer rather than by a hand-written check. Advanced shows the model.conf and policy.csv they compile to, and takes extra policy lines of your own:
p, claude-code, model:claude-opus-*, use, deny p, claude-code, model:*, use, allow p, claude-code, tool:shell/run, use, deny p, claude-code, tool:mcp/github, use, allow p, claude-code, tool:mcp/*, use, deny p, claude-code, tool:*, use, allow
The first rule that matches decides, which is what lets one exception stand in front of the rule behind it: every MCP server taken away except the one that stays. The lines you write yourself are checked before the ones the switches wrote.
The rules apply to what goes through the proxy, so an agent bound directly to a provider — its own configuration pointing at the vendor rather than at Gateway — is not held to them. The page says so where that is the case.
An agent on its own subscription relays nothing through Gateway, and a request that goes straight to the vendor leaves no record here — but the agent writes a transcript of it on disk anyway. Usage reads those transcripts, so the spend of every agent on this machine is on one page from the first start, with no provider configured and nothing routed: tokens, cache hit rate and cost, over time and broken down by model and by agent. The second tab, What Gateway relayed, is the narrower account of the traffic that did come through, and the only one that knows which provider answered and whether it failed.
Every figure there is worked out from Model pricing, which is a table of what a million tokens costs. Vendors change their prices and resellers do not follow, so a price can be edited by hand, and Gateway can reprice the models this machine has run from the models.dev catalogue on a schedule (modelsDevSyncMode, modelsDevSyncIntervalHours), leaving anything you edited yourself alone.
casbin-gateway stop. Start again: casbin-gateway start. Check: casbin-gateway status. All three work from any directory — the command is a wrapper that always starts Gateway in its install directory, where its data lives.casbin-gateway, stopped with Ctrl-C. In the background its console output goes to logs/casbin-gateway.out.casbin-gateway uninstall first. Deleting the directory is the part you can do yourself; what it cannot undo is everything Gateway wrote outside it — the monitoring hooks in every agent's own configuration, the provider each agent was pointed at, the login entry, the desktop entries and the ccswitch:// scheme, all of which would otherwise be left naming a program that is no longer there. It puts all of that back, then names the two things to delete by hand: ~/.local/share/casbin-gateway and ~/.local/bin/casbin-gateway (on Windows, %LOCALAPPDATA%\casbin-gateway and its PATH entry).Set INSTALL_DIR to install somewhere else, NO_START=1 to install without starting, or NO_AUTOSTART=1 to install without starting at login.
Gateway binds 127.0.0.1 by default, because two things are wide open to whoever can reach the port: the UI signs the local admin in without asking, and /v1 relays with the API keys stored here. Both are exactly what you want from a local tool and neither should be offered to a network.
To serve other machines anyway, set httpaddr = 0.0.0.0 in conf/app.conf, and then:
/v1. Gateway generates one on first start and shows it under Settings → Security; the environment snippets on the Providers and Agents pages already carry it, and it is what Gateway writes into the configuration of an agent it switches. Requests from this machine never need it.These are nightly builds, rebuilt from master on every push and published as the nightly pre-release. They exist so that Gateway can be tried without a Go and Node toolchain; anything else should be built from a source release.
A container cannot see the agents on your machine. Agents are discovered by reading the home directories and install paths of the machine Gateway runs on, and inside a container that is the container's own filesystem. Agents, Skills, MCP & Prompts and agent monitoring therefore stay empty there, and the pages say so rather than pretending nothing is installed. Everything that does not depend on the host works normally: Providers, Authenticity and LLM Records.
So run the one-command install above on the machine whose agents you want to watch, and use a container when Gateway is only a model endpoint for other machines.
No image is published, so the compose file builds one from a checkout of this repository:
docker compose up -d
Podman reads the same file:
podman compose up -d
Either way the UI is on http://localhost:17000, the SQLite database lives in a named volume that survives down, and conf/app.conf is mounted from the repository, so the settings it seeds can be edited before the first start without rebuilding the image.
Everything is optional. Settings are changed on the Settings page of the web UI and stored in the database, so nothing has to be edited by hand and nothing has to be restarted. conf/app.conf, next to the executable, seeds them on the very first start and explains each one; the one-step install has no file beside it and seeds from the copy baked into the binary instead. Editing the file after that first start does nothing, except for the keys read before the database is open: httpport, driverName, dataSourceName, dbName and redisEndpoint. The settings people actually change:
| Setting | Default | What it does |
|---|---|---|
httpport | 17000 | Port of the web UI and the REST API |
httpaddr | 127.0.0.1 | Interface the web UI binds to — see Serving other machines |
driverName / dataSourceName | sqlite / ./data/casbin-gateway.db | Where data is stored |
llmRecordMode | full | How much of each relayed LLM request is kept — including the prompt, see Recording prompts |
providerProbeMode | auto | Whether providers are probed for authenticity on their own, only when asked (manual), or never (off) |
apiKeyEncryptionKey | empty | Encrypts provider API keys at rest (AES-256-GCM) |
casdoorEndpoint | empty | Switches sign-in over to Casdoor SSO |
Gateway prints what it is actually doing when it starts, so the result can be checked instead of the file:
+---------------------------------------------------------------------+ | Casbin Gateway | +---------------------------------------------------------------------+ | Management UI | http://localhost:17000 (this machine only) | | Settings | Settings page, seeded from conf/app.conf | | Web UI files | web/build | | Database | sqlite, file "./data/casbin-gateway.db" (connected) | | Sign-in | built-in user table, Casdoor is not configured | | Relay auth | this machine only, no token needed | +---------------------------------------------------------------------+
A previous Gateway still holding this port is stopped first, so a restart never waits on it. A port held by anything else stays with that program: Gateway names the process holding it and stops, rather than taking the port or starting half-configured.
Every relayed request is recorded in full from the first one, prompt included. That is what the LLM Records page is made of, and none of it leaves this machine — but a prompt carries whatever was pasted into it, so if that is not what you want, this is the setting to change before you route an agent through Gateway. The picker at the top of the LLM Records page switches between Recording off, Record metadata and Record metadata and bodies, and takes effect from the next request; the Settings page holds the same choice and the limits around it, seeded from:
; The default is "full": it stores the request body, which is what LLM Records ; needs to show prompts, messages and tool schemas. "metadata" records only who ; called which model with which outcome, and "off" keeps nothing at all. llmRecordMode = "full" llmRecordRetentionDays = 30 llmRecordMaxRecords = 10000 llmRecordMaxPayloadBytes = 1048576
Bodies are sanitized before they are stored: anything that looks like a credential is replaced, and the number of replacements is shown with the record. Request headers, which is where the inbound API key is, never reach a record at all. A body over llmRecordMaxPayloadBytes keeps its structure and loses only its longest strings, so a large conversation is still listed message by message.
The cost next to each record uses list prices, which vendors change and resellers do not follow. Correct them on the Model pricing page, or point llmPricingFile at a JSON file of your own rates.
Casdoor is optional and takes over member management. Create an organization and an application for Gateway in a Casdoor instance, then fill in the five fields of Settings → Sign-in. Sign-in redirects to Casdoor as soon as casdoorEndpoint is set, which also enables OAuth logins.
Go 1.25+, and Node.js with Yarn.
The backend serves the compiled frontend out of web/build, so build it once first:
cd web && yarn install && yarn build
go run main.go
Then open http://localhost:17000, where you are signed in as the local admin, same as an installed Gateway. The SQLite database is created on first start; there is no database server to install.
cd web && yarn dev
That serves the UI on http://localhost:16002 with hot reload and proxies API calls to the backend on port 17000, so both have to be running.
XORM is used, so every database it supports works. Point Gateway at your server and it creates dbName on first start if it does not exist:
driverName = mysql dataSourceName = root:123@tcp(localhost:3306)/ dbName = casbin_gateway
Gateway normally reads two things from disk: conf/app.conf and the compiled UI in web/build. The embed build tag bakes both into the executable, which is what the install scripts ship:
cd web && yarn install && yarn build
go build -tags embed -o casbin-gateway .
Build the frontend first — everything under web/build goes into the binary, so go build -tags embed fails to compile while that directory is missing.
Files on disk always win over the embedded copies, so a single binary can still be configured and developed against without rebuilding it:
| Embedded asset | Overridden by |
|---|---|
conf/app.conf | conf/app.conf in the working directory, or next to the executable |
web/build | web/build/index.html in the working directory, which then serves the whole UI |
The startup summary reports which source each one came from.
Being self-contained is about startup, not about staying read-only. A running Gateway writes ./data (the SQLite database and agent patch state), ./logs and ./tmp relative to its working directory — which is why the installed casbin-gateway command is a wrapper that always starts it in its install directory. Running the executable directly from somewhere else gives you a second, empty installation there.
Casbin Gateway contains 2 parts:
| Name | Description | Language | Source code |
|---|---|---|---|
| Frontend | Web frontend UI for Casbin Gateway | TypeScript + React + shadcn/ui | https://github.com/apache/casbin-gateway/tree/master/web |
| Backend | RESTful API backend for Casbin Gateway | Golang + Beego + XORM | https://github.com/apache/casbin-gateway |
User Manual — every page of the UI, from installing Gateway to routing an agent through it.
If you have any questions, open an issue, or start a pull request directly — though we recommend opening an issue first to talk it through with the community.