Skip to content

// 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.

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.

The dashboard can classify a command without running it. The check is free, and it reads nothing but the command text:

Terminal window
$ 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.

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.

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:

Terminal window
$ 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.

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:

Terminal window
$ 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:

Terminal window
$ 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.

Terminal window
$ 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"}
  • The stage-into-a-terminal endpoint cannot succeed. POST /api/terminal/sessions/{id}/stage is 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 answers 409 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.