Back to Blog
ARTICLE

A console that refuses first and acts second

September 30, 2026
Milad Nalbandi
2 min read
1 reader
keel

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.

The dashboard: one local page for every keel project on the machine

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:

  1. The config switch — off by default, and off again after an upgrade
  2. The host — must be loopback, or a service named in your compose file
  3. SQL — must be a single read
  4. Commands — named by key from your config, never written out in the request
  5. 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

Milad Nalbandi
Software Engineer & Writer