// Concept
Model locality and the secure tier
Halbert ships no model. It offers slots, and each slot points at a connection you made: a runtime on this machine, or a service somewhere else. Which of those a slot points at decides the thing the rest of the design rests on — whether the contents of a turn stay here.
That deserves a page of its own, because the obvious way to answer it is wrong, and because the machine sometimes has to override your own choice to keep the answer true.
Loopback is not locality
Section titled “Loopback is not locality”The intuitive test is the address. If the request goes to localhost, the model must be running
here. It is the test the code used to apply, and it is false.
A model runtime on this machine can serve a model it does not run. The daemon accepts the request
at localhost, forwards the prompt to the vendor, and returns the answer — so the address is
local, the connection is local, and the prompt is not. The convention that marks such a model is a
suffix on its name, :cloud, and the name is therefore the primary evidence. The address is the
weakest of the three things worth checking.
So the rule has three clauses, and all of them must hold: no cloud tag, a provider that runs models on this machine’s own runtime, and a loopback address — necessary, never sufficient. Run against the one function that applies them, the two rows that carry the argument come out like this:
| Model name | Address | Provider | Local? |
|---|---|---|---|
example-7b |
http://localhost:11434 |
ollama |
yes |
example-7b:cloud |
http://localhost:11434 |
ollama |
no |
Same address, same connection, opposite answers, and the name is the only thing that changed. Connect a local model has the full set of cases and the three things on the models screen that look like they apply this rule and do not.
Two of those cases are worth stating here because they are the design rather than edge handling. An
unnamed model is not local, because the name is where the tag would be, so an unnamed one
cannot be proven anything. And http://attacker.example/localhost is not local, because the
address is parsed as a URL rather than searched for a word. Anything unknown reads as not local,
everywhere: on this particular question a default that reads as local is the whole bug.
One judge, not a check at every door
Section titled “One judge, not a check at every door”The rule lives in exactly one function. Everywhere else that needs the answer calls it, including the gate that runs at turn time, which is a delegate rather than a second opinion and is meant to stay one.
This is not tidiness. The locality check was duplicated — the turn-time gate had its own idea, and its idea was the address. On the machine this software was written on, both the conversational slot and the specialist slot held cloud-tagged models on a loopback connection, so every secure turn that fell back to them left the machine. Nothing was misconfigured in a way anyone could see; the second implementation was simply older than the problem.
A rule with two implementations is a rule with one of them wrong, and no way to notice. One judge means a correction lands everywhere at once.
The same function is applied when settings are saved, not only when a turn runs. Assign a cloud-tagged model to the secure slot and the assignment is stored disabled rather than refused, so the interface can still show what was configured:
secure_model 'example-7b:cloud' @ 'http://localhost:11434' (ollama) is not a local model;slot disabled (a ':cloud' tag or non-loopback endpoint is cloud-bound whatever the URL says)What makes a turn secure
Section titled “What makes a turn secure”A slot is a standing choice. A turn is a single exchange, and a turn can turn out to be carrying something the standing choice was not made for — a token in a file you asked about, a key in the output of a command you ran.
The machine decides that for itself, on the assembled context, and the check that decides it is content-based: it reuses the redactor as a detector. If scrubbing the context would change it, the context holds something the scrubber considers a credential. Run on three inputs:
'How much disk do I have left?' secure=False'export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7M' secure=True'the token is ghp_0123456789abcdefghijklmnopqrs' secure=TrueThe detector fails toward secure=True, including when it raises: a false positive costs a
local-model answer, and a false negative ships a secret to a vendor. Those are not the same price.
Once raised, the flag latches for the whole turn. Planning, the answer, and any re-entry after a tool runs are all resolved under it — a turn cannot start secure and finish somewhere else.
What a secure turn does
Section titled “What a secure turn does”The boundary beats everything, including a model you pinned yourself. In order:
- The dedicated secure slot, if this machine has one configured and it is genuinely local.
- The model the turn had already chosen, if that one is local — the common case, and nothing visible happens.
- The local conversational model, as a fallback, with the reason recorded: the model you asked for is cloud-bound and this turn is not allowed to use it.
- Nothing. If no local model can answer, the turn stops:
This turn contains sensitive content (secrets or credentials) and no local model isconfigured to answer it. Configure a local guide or secure model in Settings → AI Models.Step 4 is the one worth understanding before you need it. On a machine whose only connections are remote, a question that touches a credential does not get a worse answer — it gets no answer, and a sentence explaining why. That is the intended behaviour, not a failure to handle a case.
Secrets are described, never summarised
Section titled “Secrets are described, never summarised”The other half of the boundary is that a secret does not need a model in the first place. Asked about a configuration value, Halbert classifies it — public, operational, or a credential — and a credential is answered by a deterministic description rather than by anything generative:
{ "key": "api_token", "file": "/etc/halbert/models.yml", "length": 42, "charset": ["lowercase", "uppercase", "digits", "symbols"], "entropy_bits": 5.25, "view_command": "grep api_token /etc/halbert/models.yml", "redacted": true, "last_changed": null, "credential_type": { "type": "github_pat_classic", "service": "GitHub", "description": "GitHub Personal Access Token (classic)", "confidence": "high", "breach_risk": "high", "validation_available": true }, "breach_risk": "high"}That tells you what you actually wanted to know — which service it is for, how strong it is, that it is still there — and the value never enters a prompt. The last useful thing it does is hand you the command to look at the value yourself, which is the correct division of labour: you have the machine already.
Scrubbing happens before the model, never after it. Asking a model to summarise a payload without repeating the secret in it is a request that can only be mostly obeyed, so it is never made — and where a template can answer, a template answers.
What is not there yet
Section titled “What is not there yet”- The detector reads context, not your typing. It inspects what tools and retrieval brought
back, which is where a secret usually arrives. Pasting a credential straight into the composer is
not what raises the flag: the same text raises
secure=Trueas a tool observation andsecure=Falseas the query alone, measured both ways. If you are pasting a secret to ask about it, pin a local model first. - The provenance half of the detector never fires. It is written to mark a turn whose context
drew on the unredacted view of this host’s own files, and it works when it is given where a chunk
came from. The one place that calls it hands it the kind of each chunk instead —
retrieval,memory,observations— and no kind is a location, so the clause matches nothing. The content check above is what is protecting the turn. - Nothing catches the refusal by name. A turn that fails closed raises out of model resolution, and the sentence above is the whole of the user-facing treatment — there is no screen designed for that state.
- The reason for a disabled secure slot is only in the log. On screen the row keeps showing the model you chose and simply stops claiming to be local.
Guardrails and approvals is the same question about actions instead of text. Capabilities, not variants explains why configuring a local model turns a feature on with no flag edited in between.