# NousResearch/hermes-agent · is_local_endpoint detection of Docker/Podman and local hostnames

## Open now

- 1 issue, 1 PR open on GitHub.
- #20346: \[Bug\]: using DNS for local provider is not addressed in recent fix
- PR #20359: fix(agent): treat mDNS/.lan/unqualified hostnames as local endpoints (#20346)

Members concern the agent's is_local_endpoint check not recognizing DNS-based local endpoints, such as Docker/Podman DNS names, mDNS/.lan names, and unqualified hostnames, with a related stale timeout affecting local LLM proxies. Several items are fixes proposed for this theme, and one is a bug report saying a recent fix did not cover DNS for local providers.

## TL;DR

- 7 member(s): 1 open issue(s), 1 closed issue(s), 1 open PR(s), 2 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #9248. Verify it addresses your case.
- **Recommended:** read PR #9248 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-9248`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.26`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #20346: \[Bug\]: using DNS for local provider is not addressed in recent fix

## Closed / duplicate issues

- #7905: is_local_endpoint misses Docker/Podman DNS names — stale timeout fires on local LLM proxies

## Prior & competing PRs

- **#7950: fix: is_local_endpoint misses Docker/Podman DNS names**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/model_metadata.py`, `tests/agent/test_local_stream_timeout.py`, `tests/tools/test_tool_result_storage.py`, `tools/tool_result_storage.py`
  - fix hunks:
  - `agent/model_metadata.py`

    ```diff
    @@ -179,6 +179,12 @@ def _strip_provider_prefix(model: str) -> str:
     
     # Local server hostnames / address patterns
     _LOCAL_HOSTS = ("localhost", "127.0.0.1", "::1", "0.0.0.0")
    +# Docker / Podman / Lima DNS names that resolve to the host machine
    +_CONTAINER_LOCAL_SUFFIXES = (
    +    ".docker.internal",
    +    ".containers.internal",
    +    ".lima.internal",
    +)
     
     
     def _normalize_base_url(base_url: str) -> str:
    ```

    ```diff
    @@ -254,6 +260,9 @@ def is_local_endpoint(base_url: str) -> bool:
             return False
         if host in _LOCAL_HOSTS:
             return True
    +    # Docker / Podman / Lima internal DNS names (e.g. host.docker.internal)
    +    if any(host.endswith(suffix) for suffix in _CONTAINER_LOCAL_SUFFIXES):
    +        return True
         # RFC-1918 private ranges and link-local
         import ipaddress
         try:
    ```
  - `tests/agent/test_local_stream_timeout.py`

    ```diff
    @@ -22,6 +22,9 @@ class TestLocalStreamReadTimeout:
             "http://0.0.0.0:5000",
             "http://192.168.1.100:8000",
             "http://10.0.0.5:1234",
    +        "http://host.docker.internal:11434",
    +        "http://host.containers.internal:11434",
    +        "http://host.lima.internal:11434",
         ])
         def test_local_endpoint_bumps_read_timeout(self, base_url):
             """Local endpoint + default timeout -> bumps to base_timeout."""
    ```

    ```diff
    @@ -68,3 +71,38 @@ def test_empty_base_url_keeps_default(self):
                 if _stream_read_timeout == 120.0 and base_url and is_local_endpoint(base_url):
                     _stream_read_timeout = _base_timeout
                 assert _stream_read_timeout == 120.0
    +
    +
    +class TestIsLocalEndpoint:
    +    """Direct unit tests for is_local_endpoint."""
    +
    +    @pytest.mark.parametrize("url", [
    +        "http://localhost:11434",
    +        "http://127.0.0.1:8080",
    +        "http://0.0.0.0:5000",
    +        "http://[::1]:11434",
    +        "http://192.168.1.100:8000",
    +        "http://10.0.0.5:1234",
    +        "http://172.17.0.1:11434",
    +    ])
    +    def test_classic_local_addresses(self, url):
    +        assert is_local_endpoint(url) is True
    +
    +    @pytest.mark.parametrize("url", [
    +        "http://host.docker.internal:11434",
    +        "http://host.docker.internal:8080/v1",
    +        "http://gateway.docker.internal:11434",
    +        "http://host.containers.internal:11434",
    +        "http://host.lima.internal:11434",
    +    ])
    +    def test_container_dns_names(self, url):
    +        assert is_local_endpoint(url) is True
    +
    +    @pytest.mark.parametrize("url", [
    +        "https://api.openai.com",
    +        "https://openrouter.ai/api",
    +        "https://api.anthropic.com",
    +        "https://evil.docker.internal.example.com",
    +    ])
    +    def test_remote_endpoints(self, url):
    +        assert is_local_endpoint(url) is False
    ```
  - `tests/tools/test_tool_result_storage.py`

    ```diff
    @@ -124,6 +124,34 @@ def test_uses_parent_dir_of_remote_path(self):
             cmd = env.execute.call_args[0][0]
             assert "mkdir -p /data/data/com.termux/files/usr/tmp/hermes-results" in cmd
     
    +    def test_path_with_spaces_is_quoted(self):
    +        env = MagicMock()
    +        env.execute.return_value = {"output": "", "returncode": 0}
    +        remote_path = "/tmp/hermes results/abc file.txt"
    +        _write_to_sandbox("content", remote_path, env)
    +        cmd = env.execute.call_args[0][0]
    +        assert "'/tmp/hermes results'" in cmd
    +        assert "'/tmp/hermes results/abc file.txt'" in cmd
    +
    +    def test_shell_metacharacters_neutralized(self):
    +        """Paths with shell metacharacters must be quoted to prevent injection."""
    +        env = MagicMock()
    +        env.execute.return_value = {"output": "", "returncode": 0}
    +        malicious_path = "/tmp/hermes-results/$(whoami).txt"
    +        _write_to_sandbox("content", malicious_path, env)
    +        cmd = env.execute.call_args[0][0]
    +        # The $() must not appear unquoted — shlex.quote wraps it
    +        assert "'/tmp/hermes-results/$(whoami).txt'" in cmd
    +
    +    def test_semicolon_injection_neutralized(self):
    +        env = MagicMock()
    +        env.execute.return_value = {"output": "", "returncode": 0}
    +        malicious_path = "/tmp/x; rm -rf /; echo .txt"
    +        _write_to_sandbox("content", malicious_path, env)
    +        cmd = env.execute.call_args[0][0]
    +        # The semicolons must be inside quotes, not acting as command separators
    +        assert "'/tmp/x; rm -rf /; echo .txt'" in cmd
    +
     
     class TestResolveStorageDir:
         def test_defaults_to_storage_dir_without_env(self):
    ```
  - `tools/tool_result_storage.py`

    ```diff
    @@ -24,6 +24,7 @@
     
     import logging
     import os
    +import shlex
     import uuid
     
     from tools.budget_config import (
    ```

    ```diff
    @@ -79,7 +80,7 @@ def _write_to_sandbox(content: str, remote_path: str, env) -> bool:
         marker = _heredoc_marker(content)
         storage_dir = os.path.dirname(remote_path)
         cmd = (
    -        f"mkdir -p {storage_dir} && cat > {remote_path} << '{marker}'\n"
    +        f"mkdir -p {shlex.quote(storage_dir)} && cat > {shlex.quote(remote_path)} << '{marker}'\n"
             f"{content}\n"
             f"{marker}"
         )
    ```
- **#8021: fix(agent): detect Docker/Podman DNS names in is_local_endpoint**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#7906`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#9248: fix(local): recognize unqualified hostnames as local endpoints**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/model_metadata.py`, `tests/agent/test_local_stream_timeout.py`
- **#7906: fix: is_local_endpoint misses Docker/Podman DNS names**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#7950`, `#9248`
- **#20359: fix(agent): treat mDNS/.lan/unqualified hostnames as local endpoints (#20346)**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#9248`
  - files: `agent/model_metadata.py`, `scripts/release.py`, `tests/agent/test_local_stream_timeout.py`

## Fix status

- Merged to main in PR #9248 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-05`.
- Subsystem: `agent`
