The keel dashboard was read-only. Adding a console to it — call an endpoint, read the database, run a command — is the first write path in the whole server, and it deserved more care than a feature flag.
Why a localhost page is not automatically safe
The instinct is that loopback is private. It mostly is — but a page you have open in another
tab can POST to 127.0.0.1 even without being able to read the response. With
mode: 'no-cors', the browser sends it and simply hides the answer. Hiding the answer doesn't
matter when the request itself is the point.
hostOk blocks DNS rebinding. It does not block that.
So the console isn't guarded by one check. Four barriers guard every POST, and then five gates guard every action:
- The config switch — off by default, and off again after an upgrade
- The host — must be loopback, or a service named in your compose file
- SQL — must be a single read
- Commands — named by key from your config, never written out in the request
- The phase guard — the command still passes through
guards.checkBash
That fifth one is the one I'd argue for hardest. The console doesn't re-implement the rules about what may run in which phase — it inherits them. A command the guard refuses during RED is refused here too, for the same reason, with the same message. Two copies of that logic would drift, and the copy that drifted would be the one with the power.
Named by key, never written out
The command endpoint doesn't take a command. It takes a key:
{ "command": "ci" } ✓ looked up in your config
{ "command": "rm -rf /" } ✗ not a key, so not a command
Your config already declares what this project can run. The console can only pick from that list. That turns "run a command" from an open door into a menu — and the menu is one you wrote.
Off is the default, and stays the default
console:
enabled: false
endpoints: 'off' # off | read | write
db: 'off' # off | read — there is no write
queue: 'off'
commands: 'off'
Note db has no write level at all. Not "off by default" — absent. A read-only database
console is a debugging tool; a writable one is a foot-gun with a web interface.
And the whole block resets to off after an upgrade. A permission you granted to version N is not a permission you granted to version N+1, which does things N didn't.
Everything that runs leaves a trace
Every console action lands in the event log — the same feed the dashboard already shows.
This is the part that makes the rest defensible. A power that leaves no trace is the actual escalation: not that something ran, but that nobody can find out afterwards what ran, or when, or from where. If it executed, it's in the log.
Turning it on
Per project, in .keel/config.yml, deliberately. There's no global switch and no UI toggle —
turning it on is an edit to a file you own, in a repo you can diff.
That friction is the feature.
keel is a Claude Code plugin. github.com/MiladNalbandi/keel

