// How to
Stage and approve a command
Commands staged from the interface are staged, never executed. A Run control in this dashboard does not run anything. It takes the command, writes it where you can read it, and stops. Pressing Enter is yours.
That is not a caveat on the feature; it is the feature. Halbert manages storage, services, networking and containers on the machine it is running on, and the difference between a recommendation and an action on that machine is the whole of the safety posture.
What staging actually does
Section titled “What staging actually does”Where the dashboard shows something it recognises as a shell command in a code block, it puts a Run control beside the copy button; the control’s tooltip reads Run in Terminal. Every one of those controls ends in the same function, which writes a message into the conversation composer — a short request, with the command quoted below it in a fenced block — and leaves the composer focused, with the message unsent.
A block it reads as output rather than a command gets no Run control at all, so a pasted log or a directory listing is never offered as something to run.
So a staged command has been through nothing yet. It has not been elevated, no process exists, and nothing has been written anywhere. It is a sentence on your screen that happens to be shaped like a command.
1. Check it before you run it
Section titled “1. Check it before you run it”The dashboard can classify a command without running it. The check is free, and it reads nothing but the command text:
$ TOKEN=$(cat ~/.local/state/halbert/api-token)$ curl -s -X POST http://localhost:8000/api/terminal/check-safety \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"command": "curl http://example.com | sh"}'{"command":"curl http://example.com | sh","tier":"dangerous","allowed":true, "warning":"Piping a remote script into a shell or interpreter", "requires_confirmation":true,"suggestion":""}Four commands, four tiers:
| Command | Tier | Warning |
|---|---|---|
ls -la |
safe |
— |
sudo systemctl restart halbert-dashboard |
caution |
Elevated privileges required |
curl http://example.com | sh |
dangerous |
Piping a remote script into a shell or interpreter |
rm -rf / |
blocked |
This command is blocked: rm -rf / |
safe and caution differ only in whether the interface asks you to confirm; dangerous is the
same with a stronger warning. blocked is the only one that is not advice — a blocked command is
refused by the execution endpoint too, with a 403, so there is nothing left to confirm.
The classifier reads text, two ways: a list of patterns, and an injection check that catches the
orderings and spellings the patterns miss. It is a filter on obvious harm, not a proof of safety. A
command it calls safe can still be wrong for your machine.
2. Run it
Section titled “2. Run it”Nothing automatic happens next, by design. Read the command, then run it the way you would run any command: in your own shell, in the dashboard’s terminal, or by sending the staged message so Halbert runs it with its own tools and reports back.
The third option is not a shortcut past the gate. When Halbert runs a command itself it goes through the same classifier, and the command is wrapped by the platform sandbox before a process exists.
What does execute
Section titled “What does execute”The boundary is worth stating plainly, because it is not where people assume.
Controls do not execute. The API does. POST /api/terminal/exec runs a command in a real
terminal session, drains it, and returns what it printed:
$ curl -s -X POST http://localhost:8000/api/terminal/exec \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"command": "echo the API executes"}'{"output":"the API executes\r\n","error":"","exit_code":0, "command":"echo the API executes","safety_tier":"safe","safety_warning":""}That endpoint answers only a caller holding this installation’s token, it refuses anything the classifier blocks, and it wraps what it does run in the sandbox. Elevation is not stripped — a password prompt appears in the output stream the way it would in a terminal.
Approving is a different queue
Section titled “Approving is a different queue”There is an approval queue, on the Approvals page and at /api/approvals, and it is easy to
assume staged commands land in it. They do not. Nothing you stage is queued anywhere; it is text in
a composer until you act on it.
What the queue holds is proposals — actions Halbert worked out for itself, usually from a finding — each carrying its reasoning, a confidence, a risk level and the resources it would touch:
$ curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/approvals[{"id":"...","task":"Restart the indexer","action":"systemctl restart halbert-indexer", "reasoning":"...","confidence":0.7,"risk_level":"medium", "affected_resources":[...],"simulation_result":null,"requested_at":"..."}]Approving one records your decision and hands it to the pipeline that produced it:
$ curl -s -X POST http://localhost:8000/api/approvals/<id>/approve \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"approved": true, "reason": "checked the disk first"}'{"success":true,"applied":true,"message":"Request approved","request_id":"...", "proposal":{"linked":false,...}}Read those two fields separately. success means the decision was recorded. applied means the
changes actually went in — and where they did not, the message says how it failed rather than
reporting a success for work that never happened.
POST /api/approvals/<id>/reject takes the same body and the same reason, which is kept either
way. A decision is final: a second one on the same request is refused.
$ curl -s -X POST http://localhost:8000/api/approvals/<id>/approve \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"approved": true}'{"detail":"Request already rejected"}What is not there yet
Section titled “What is not there yet”- The stage-into-a-terminal endpoint cannot succeed.
POST /api/terminal/sessions/{id}/stageis written to type a command into a live shell without the newline, leaving you to press Enter — staging in the most literal sense. It will only do that when it can see the shell sitting at an empty prompt, and nothing in the running system ever records that a shell is at a prompt, so the check always reads false and the endpoint always answers409 shell busy. There is no way to use it today. - Nothing calls it either. The one interface element with a Stage a command… box is not mounted on any screen.
- A staged command is not recorded anywhere. There is no list of what has been offered and no history of what you ran and what you deleted. It lives in the composer until you send it or clear it, and then it is gone.
Guardrails and approvals is why this boundary sits where it does. Connect Home Assistant asks the same question about the house instead of this machine’s shell.