Air-gap
Run entirely on your own hardware, with local runners and a licence gate that fails closed.
The same four commands
Air-gap is not a separate installation, a separate binary or a separate mode you switch on. It is the Quickstart's four commands with a different argument to the last one.
npm i -g @pretense/cli@stable pretense --version pretense login --key YOUR_KEY pretense run qwen2.5-coder
Name a tool — claude — and Pretense starts the proxy and launches that tool against it. Name a model — qwen2.5-coder — and Pretense starts the same proxy and chats with the model through it. One argument decides whether your prompt reaches a hosted provider or never leaves the machine, and nothing else about the sequence changes.
Command 3 still needs a network, because it fetches your licence. A machine that will never have one imports the licence instead; that is the only step that differs, and it is below.
This page means your prompts and code never leave the machine: inference happens on a runner you control, on loopback. It does not mean the CLI never opens a socket. Installing it uses the network, pretense login uses the network, and usage counts are reported unless you turn them off — including for a request that was refused. Set PRETENSE_USAGE_REPORT=off to stop that. For a machine with no route out at all, see the import step below.
The licence gate
Air-gap is an Enterprise capability and it fails closed. Without a valid licence every request is refused with 403 pretense_enterprise_required. That is correct behaviour, not a fault.
pretense login --key installs the licence for you. See the Quickstart.
Export the licence where there is a network and carry the file across: pretense auth licence --import licence.jwt.
The proxy boots and prints its air-gap banner whether or not a licence is installed, so the banner is not proof that air-gap is working. The first request is.
Routing
Nothing to configure for the common case, on a current CLI. If a runner is listening on loopback, Pretense finds it: it probes Ollama, vLLM, LM Studio and llama.cpp on their usual ports, routes the model to whichever one actually serves it, and refuses if none does.
ollama pull qwen2.5-coder pretense run qwen2.5-coder
Zero-config discovery is the paragraph above; the fail-closed half of it is what the version gate is about. On 0.6.58, which is what npm i -g @pretense/cli@stable installs today, a model that no local runner serves does not get refused — with no pretense.yaml present the request is forwarded to a hosted provider instead. Run pretense --version and read the number.
Until you are on 0.6.59, do not rely on discovery for isolation. Declare the runners explicitly and set airgap.enabled: true, as in the block below — that configuration fails closed on 0.6.58, refusing with 403 pretense_enterprise_required or pretense_no_local_runner rather than reaching out. It is the safe configuration on every version, which is why it is documented here rather than retired.
Declare runners explicitly when the default is not enough — a runner on a non-standard port, a GPU box on your own network, or several runners you want to route between. See Configuration.
airgap:
enabled: true
runners:
- name: local-ollama
baseUrl: http://127.0.0.1:11434
models: ["llama*", "qwen*", "deepseek*"]baseUrl points at the runner's OpenAI-compatible surface. Ollama, LM Studio, vLLM and LocalAI all serve one, on the same host and port as their own API.
Routing is then by model name. Each glob compiles to an anchored, case-insensitive regex, so the glob has to be a prefix of the tag.
| Model tag | Matched by | Reaches your runner |
|---|---|---|
llama3.3:70b | llama* | yes |
qwen2.5-coder | qwen* | yes |
codellama | no default glob | needs an entry |
qwq | no default glob | needs an entry |
Omit models: and every local family is matched: llama*, codellama*, mistral*, mixtral*, codestral*, gemma*, codegemma*, qwen*, qwq*, deepseek* and phi* — old tags and new. A family must be followed by a version character, so mistralai and llamaindex are not claimed.
It does not extend them. Adding ["codellama*"] stops routing llama3.2. Spell out every family you use.
Once airgap.enabled is set, a model that matches nothing is refused with pretense_no_local_runner. There is no fall-back to a hosted provider, and a typo in baseUrl is a hard stop rather than quiet cloud egress. That is the guarantee worth having, and it is the one the explicit block above buys you on every shipped version.
Verify isolation
Do not trust the setting. Read the audit log.
pretense start
curl -s http://localhost:9339/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"llama3.2","messages":[{"role":"user","content":"hi"}]}'
pretense audit --limit 20 --jsonThe model tag in the request is what decides where it goes, so the command is the same online and air-gapped. While traffic stays on your runner, every row's provider reads openai-compatible. A row naming anthropic, openai or google went to a hosted provider.
Confirm it at the network layer as well, with tcpdump or a firewall deny rule. A proxy that has been tampered with is not a trustworthy witness to its own egress.