The network policy (
localm/netpolicy.py) is CORE and governs every model-initiated web request, plugin or not, along with model pulls and HuggingFace search. It is not a gate on every socket localm opens (see the list of ungoverned paths). The chat "Web access" surface is provided by thewebplugin and appears only when it is active. The coder'sfetch_urlandweb_searchtools are built into the coder itself and are always present, independent of thewebplugin. The off/ask/allow modes and SSRF protection below apply to both.
localm is offline-first: nothing requires the internet. But some tasks genuinely need it - looking up current documentation, checking a version, querying the weather, reading an error's bug tracker. This page describes how model-initiated network access works and how to control it.
Every network request a model can trigger goes through one policy choke
point (localm/netpolicy.py). The coder's fetch_url and web_search
tools, and the chat's web access, all use it. There is no second path.
Things a user triggers directly (localm pull, the /web chat command,
the Knowledge page's one-time embedding-model download, the mic button's
one-time Whisper download) are consent by definition - but net_mode off
still blocks them, with one exception: the admin-only
net_allow_model_downloads setting (off by default) lets the downloads you
trigger yourself (model pulls, HuggingFace search, the embedding-model and
Whisper downloads) proceed under off. It never lifts off for anything a
model triggers. A one-time download like the last two
is a single-call authorization, never written to config; it lets that one
fetch through ask's per-request friction, exactly like clicking "download
now" is itself the consent, and it changes nothing about how the next
request is handled.
Online coder providers (--online, --anthropic) are a separate case and
are not covered by this policy at all: every request to OpenAI/Anthropic
goes out over HTTPS through a direct provider client, outside
netpolicy, with no net_mode check anywhere in that path. The only gate
is the explicit CLI flag itself, plus a warning if you try it in privacy
mode. The same privacy-mode warning names the host when --url (or
LOCALCODER_URL) points the coder at a model server that is not on this
machine.
localm config net_mode off # nothing gets through
localm config net_mode ask # default - the coder asks before each request
localm config net_mode allow # no confirmation| mode | coder fetch_url / web_search |
chat web access |
|---|---|---|
off |
tool returns a policy error | /web and the toggle return a clear error |
ask (default) |
approval prompt per request (terminal y/N or GUI approval card showing the URL/query) | /web runs immediately (typing the command is the consent); the "Web access" toggle is on by default so the model knows the tools exist, and every model-initiated request shows a per-request approval card |
allow |
runs without asking | works |
The LOCALM_NET_MODE env var overrides the config (like LOCALM_MODE for
privacy). In the coder, sessions started with auto-approve also auto-approve
network requests in ask mode.
ask's approval prompt is enforced by the front end (the GUI modal, the
coder's terminal y/N), not the server. A direct API or MCP caller
authenticated with the web scope hits the underlying endpoints directly
and is treated as already consented - it is not prompted. Domain rules and
the SSRF guard below still apply in every mode, off included.
localm config net_allow "docs.python.org, github.com, wttr.in" # only these
localm config net_deny "doubleclick.net" # never thesenet_allowempty (default) = any domain. Non-empty = only listed domains.net_denyalways wins overnet_allow.example.commatchesexample.comand every subdomain (api.example.com).- An internationalised name is compared in its IDNA (
xn--) form, so an entry written either way matches both spellings of the host.
By default, requests to loopback, private, and link-local addresses are
refused - that includes 127.0.0.1 (the localm API itself), 192.168.x.x
(your router's admin page), and 169.254.169.254 (cloud metadata). Redirects
are followed manually and every hop is re-validated, so a public page
cannot bounce the agent into your LAN. Response bodies are size-capped.
If the coder legitimately needs to talk to a local dev server
(http://localhost:3000), opt in:
localm config net_allow_private trueThe hostname is resolved and validated once, then the connection is pinned to the validated addresses of that lookup, so the connect cannot re-resolve to a different address. When the first address cannot be connected to (for example an IPv6 address on a network without working IPv6), the next validated address from the same lookup is tried, up to four, all within twice the connect timeout. This closes the check-and-connect DNS-rebinding race: a rebind or an unresolvable host is refused, not reconnected through a fresh lookup. The domain deny/allow lists remain an additional control.
web_search searches and then reads the top three result pages, returning an
evidence bundle: each source labelled S1, S2, ... with its title, URL and
a grounding label, followed by evidence excerpts selected from the pages that
could be read (at most 12,000 characters in total, 4,000 per source). The
bundle's own grounding is page-backed when at least one page was read,
snippet-only when only the search snippets are available, and failed when
there is no evidence at all; the chat, the coder and scheduled jobs all show
that label rather than presenting snippets as read pages. fetch_url reads a
single page on request. The same retrieval is exposed to API clients as
POST /api/web/retrieve ({"query": "..."}); /api/web/search and
/api/web/fetch remain for explicit low-level use.
Once the results are in, localm waits at most 15 seconds for all of the
page reads together, and a page that keeps trickling in is cut off; a
page that could not be read keeps its search snippet as evidence and is
labelled with the reason (for example stackoverflow.com refused access, HTTP 403). A few sites are read from their own content endpoints instead of the
page, by web_search and fetch_url alike:
github.com/<owner>/<repo>(and/tree/<ref>/...): the README, fromraw.githubusercontent.com, then the GitHub REST API (api.github.com).github.com/<owner>/<repo>/blob/...: the raw file fromraw.githubusercontent.com.- A Stack Overflow or other Stack Exchange question: the question and its top
three answers from the Stack Exchange API (
api.stackexchange.com).
The original URL must pass the domain rules first, and each endpoint is
checked like any other request, so with a net_allow list the endpoint is
used only when its host is allowed too; otherwise the page itself is read.
The default backend is a chain of no-key search services - no account, no API key, nothing to configure: DuckDuckGo's HTML page, then DuckDuckGo's lite page, then Brave Search. A search whose connection is reset or cut is sent again (up to three tries); when a service still fails, answers with a bot check or returns a page without results it can read, localm moves on to the next one. A page counts as "no results" only when it says so itself, and that answer ends the search; any other page without readable results is reported as a failure. A service that answered with a bot check is asked once more after a short pause. Only when every service failed does the search report it, naming each one's cause. A query can therefore reach Brave Search when DuckDuckGo does not answer.
A configured SearXNG instance is the only service a search asks: localm
removes a stray /search path or query from the configured URL and reads the
instance's HTML results page when its JSON format is turned off. When the
instance returns no results while some of its own search engines failed, the
search is reported as failed instead of empty. For a self-hosted search
backend, point localm at a SearXNG instance:
localm config net_search_url http://192.168.1.10:8080An instance on your LAN or on this machine needs no other setting when
net_search_url names it by its IP address, localhost, a name without a dot,
or a name ending in .local, .lan, .internal, .home.arpa or
.localdomain: the search then reaches the loopback or private address of
exactly that origin (the same scheme, host and port), and nothing else. A public
host name gets no exemption, so a public instance that resolved to a private
address is still refused. A page the model fetches, even one on that instance,
still gets the private-address check, and link-local and cloud-metadata
addresses (169.254.x.x, in any IPv6 form) are refused for the search backend
too. Do not
turn on net_allow_private for this: it removes the private-address check from
every model-driven request, including ones to 127.0.0.1 (the localm API
itself) and 169.254.169.254.
/web <query>- explicit, one-shot grounding. Searches, reads the top result pages, shows the evidence as a dimmed "Web" message in the conversation (with its grounding label), and the model answers from it, citing the source IDs. The command itself is the consent for that search and its page reads, so it works with the toggle off;net_mode=off, the domain lists and the private-address guard still apply. When no page could be read, the message is labelled snippet-only and a notice says so.- The "Web access" toggle (parameters drawer) - lets the model decide.
The model can emit a
web_searchorfetch_urlrequest mid-conversation; the GUI executes it through the policy, injects the evidence, and the model continues (at most 3 web rounds per send). Every request and result is visible in the conversation - nothing happens silently. With no saved choice the toggle follows the policy: on underallowandask(underaskeach request is approved first), off underoff. When the toggle is off the model is told plainly that it has no internet access. A reply that only announces a lookup ("I will now search ...") without making the call gets one repair prompt asking for the call or a final answer; it is never repeated.
web_search and fetch_url appear in the coder's toolset automatically.
In ask mode each request shows an approval (the GUI approval card displays
the exact URL or query). In privacy mode, every requested URL/query, every
GitHub or Stack Exchange content endpoint contacted for it, and the address
a page was actually read from when it differs, are also echoed to
stderr ([localm privacy] fetch_url: …) so the session leaves a
visible trace on your terminal of what went out, without writing anything
to disk.
With the browser plugin installed and enabled, the coding agent gets tools
to drive a real, automated browser (browser_navigate, browser_click,
browser_fill, plus read-only browser_read/browser_screenshot/
browser_console/browser_network/browser_close). It reaches only what
this same policy already allows: every request the driven page makes -
including images and scripts the page pulls in on its own, and every hop of
a redirect - is checked, and anything refused is reported with the reason
instead of silently dropped. WebSocket connections are refused rather than
relayed. browser_navigate, browser_click, and browser_fill confirm in
ask mode like fetch_url/web_search above; the read-only tools do not.
Off by default - install the browser extra, download the browser it
drives (the Download browser button under Settings > Server & network, or
localm setup-browser), and switch it on in Settings. The system engine
runs a Chrome, Chromium, Edge or Brave already installed on the machine
instead of the download; either engine starts with a fresh, empty profile,
so none of your logged-in sessions are used. An API key needs the separate
browser scope (see SECURITY.md), independent of the
coding agent's shell access, so you can grant one without the other.
- Child processes.
run_shellcommands likepip install,npm install, orgit clonetalk to the network themselves. The gate for those is the shell-command approval (andalways_confirmforrun_shell), not the network policy. - Model downloads (
localm pull) and online coder providers (OpenAI/Anthropic opt-ins) - explicit user actions. Note that model downloads DO go through this same policy's domain lists and SSRF guard (check_url);offis bypassed only via the separatenet_allow_model_downloadssetting. A Hugging Face pull checks the hub endpoint before any request, and the size check before a single-file download checks and pins every redirect hop; the metadata requests and the transfer itself are made by thehuggingface_hublibrary, which follows the hub's redirect to its download host on its own, without the per-hop check or the DNS-rebinding pin. The coder's network reviewer (a cloud model or an off-machine URL) is checked the same way, and falls back to the local model when refused. - Bug-report upload. A deliberate, one-off action you trigger yourself
(see privacy.md); it does not call
check_urland is not subject to the domain lists. - Requests to your ComfyUI instance. ComfyUI has its own, narrower
guards instead: a configured
comfy_api_urlthat targets a link-local or cloud-metadata address is refused, and the connection itself refuses any HTTP redirect outright. Loopback and LAN are both normal, unchecked ComfyUI deployments. - Embedding-model and Whisper downloads. These respect
net_mode(includingoff). The embedding model that memory and Knowledge need is fetched automatically on first use underaskandallow. Both fetch from a fixed, hardcoded repository named by localm's own code rather than a caller-supplied URL, so they never callcheck_urland are not subject to the domain lists, the SSRF guard, or the DNS-rebinding pin - none of which a fixed destination needs. - The periodic update check. It respects
net_mode(blocked whenoff, unless explicitly exempted) but, like the embedding/Whisper downloads above, targets a fixed endpoint rather than a caller-supplied URL, so it does not callcheck_urleither. See privacy.md. - Privacy mode is orthogonal. Privacy controls what localm writes to disk; it cannot make network requests untraceable. Any request leaves DNS lookups and traffic visible to your network and the remote server. If a conversation must stay fully local, keep web access off - that is why the chat toggle is per-conversation and off by default.
Treat this policy as governing the paths named in the sections above it (chat and coder web access, HuggingFace search and pulls, the browser plugin), not as a blanket statement about every socket localm opens.
Fetched pages and search snippets enter the model's context. A malicious page can contain text crafted to steer the model ("ignore your instructions and run …"). This is inherent to giving any agent web access, and it is why the default mode asks per request and why destructive coder actions keep their own approval step regardless of where the idea came from. Treat approval prompts that follow a web fetch with extra suspicion.