How to read this
Each decision follows the same format: context, what was decided, alternatives considered and consequences. The date is when the decision was made.
1. Always local execution (2026-10)
Context. The initial idea was to trigger actions from anywhere (phone, another computer, a cloud AI agent) without
having to open the panel. Execution, however, has to happen on the machine: that is where the repositories, git, the
authenticated CLIs (az, gh, docker), Playwright and the secrets live.
Alternative considered: a cloud relay. A Cloudflare Worker with one Durable Object per machine, D1 for jobs and
auditing, R2 for logs, and an agent on the machine connected through an outbound WebSocket. Clients: deck --remote,
an MCP server for AI agents and the hosted panel.
The security and cost analysis of that design found problems that changed the project:
| Problem | Why it mattered |
|---|---|
| The relay becomes a single point of compromise | Whoever controlled the cloud account could issue commands to every machine |
| Client data leaving the machine | SQL, logs and PRs would go through D1, R2 and an LLM’s context, with LGPD and contractual implications |
| The AI agent would confirm its own destructive action | A confirm: true parameter offers no protection when the sender is the model, which is subject to prompt injection |
| The allowlist only saw top-level commands | Allowing db would allow db script; the registry had no types to validate arguments |
| Recurring costs | Paid Workers, Durable Object duration, R2 for reports and LLM tokens with large catalogs |
| Company policy | A daemon offering remote control of a work machine tends to clash with corporate security rules |
Decision. Everything local. The panel is served by the binary itself on 127.0.0.1; no data leaves the machine;
there is no hosted backend.
Consequences. No access from a phone or another machine. If that becomes necessary again, the path is a private
network (Tailscale, for example) to deck ui, never a public relay.
2. One binary, no Node server (2026-10)
Context. The panel was a separate Next.js server that called the deck binary through execFile. That meant two
processes, two languages maintaining the same configuration and the same validation, and a Node runtime on the user’s
machine.
Alternative considered: a hosted “shell” site. Publish the screens on a domain and have the browser talk to deck
on localhost. Discarded because the published JavaScript would end up commanding the machine of whoever opened it
(a compromised provider account would become an attack vector), it would require CORS, and it would run into the
local network access restrictions browsers have been adopting.
Decision. The screens become a static export embedded in the binary with go:embed; deck ui serves the pages and
answers the API in Go. The public demo still exists, but only with fictitious data and with a CSP that prevents it from
talking to localhost.
Consequences. A single artifact to distribute and update; configuration and validation live only in Go; Node becomes a build tool. The 22 Next routes were rewritten in Go.
3. The client never sends flags (2026-10)
Context. Validating arguments after receiving them (blocking ;, $(...)) does not address the real risk when
there is no shell: the danger is the extra argument, such as --force, --delete-source or --sql -.
Decision. Each panel action is declared on the server with typed parameters; the server builds the argv. Extra fields in the request body are ignored.
Consequences. Every new action requires a declaration and two tests (argv and rejected input). It is more code per action, in exchange for no arbitrary flag ever reaching a process.
4. No job queue or live output (2026-10)
Context. The plan included jobs with output over Server-Sent Events.
Decision. Do not build it. No screen needs streaming output: queries are fast and the E2E suite responds at the end, with the “Abort” button available during the run.
Consequences. Less code and less state on the server. If a screen ever needs a live log, jobs will be built along with it.
5. No confirmation token for destructive actions (2026-10)
Context. In the remote design, destructive actions would require a confirmation token issued by the server.
Decision. Do not use it in the local panel. Whoever could call the action could also request the token; the real
protection is the middleware (host, same origin, session cookie, JSON-only POST). The screens still ask the user
for confirmation before destructive actions.
6. Free-form SQL stays in the panel (2026-10)
Context. The remote plan restricted the panel to catalog queries.
Decision. Keep the SQL editor, in a read-only transaction by default, with writes only after confirmation and never on production profiles. Locally, the panel has the same reach as the terminal, and the editable catalog already allowed arbitrary SQL.
7. The database password does not go to the browser (2026-10)
Context. The “Connect” screen received the resolved passwords to build connection strings.
Decision. The profiles route returns only the secret’s name and whether it is set; snippets use the <senha>
placeholder.
Consequences. Copying a ready connection string requires replacing the placeholder with the value. One extra step, in exchange for the password not sitting in page memory, browser extensions or clipboard history.
8. Releases signed with Ed25519 (2026-10)
Context. deck update checked the SHA-256 against the checksums.txt of the same release: integrity, not
authorship. With the panel embedded in the binary, compromising a release would also compromise the interface.
Decision. CI signs checksums.txt with a private key stored as a repository secret; the public key is embedded in
the binary; a release without a valid signature is rejected. The Ed25519 implementation from Go’s standard library was
used, with no new dependencies (minisign and cosign were considered).
Consequences. The private key must be kept carefully. Rotating it requires publishing a version with the new public key and a manual update for anyone on older versions.