# NousResearch/hermes-agent · is_container() detection accuracy: false positives and misses

## Open now

- Nothing open on GitHub.

These issues and fixes center on \`is_container()\` incorrectly identifying bare hosts as containers (false positives) or failing to detect Kubernetes/containerd/cgroup-v2 environments (false negatives). Both failure modes cause downstream problems such as wrong supervision behavior, gateway crashes, and broken UI controls.

## TL;DR

- 18 member(s): 0 open issue(s), 5 closed issue(s), 0 open PR(s), 3 merged PR(s), 10 PR(s) closed without merging.
- A fix was merged to main in PR #47131. Verify it addresses your case.
- **Recommended:** read PR #47131 (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-47131,pr-105351`. 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.12`: 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.

## Closed / duplicate issues

- #44721: Kubernetes/k3s pods are not detected as containers, so \`hermes gateway start\` reports unsupported platform
- #47111: is_container() misses containerd + cgroup v2 (k8s/k3s) → s6 gateway supervision silently disabled → gateway double-start crash loop
- #48594: Dashboard update controls hidden on systemd host with Docker/containerd running
- #58135: \[Bug\]: \`is_container()\` false-positives on hosts running Docker containers (containerd markers in mountinfo), making \`home_mode: auto\` subprocess HOME non-deterministic — breaks browser auto-launch with "Chrome not found"
- #65051: terminal.backend (docker AND ssh) silently forced to local - false "running inside a container" self-detection on bare VM hosts

## Prior & competing PRs

- **#47131: fix(cli): detect containerd/CRI cgroup-v2 containers in is_container()**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_constants.py`, `tests/test_hermes_constants.py`
  - fix hunks:
  - `hermes_constants.py`

    ```diff
    @@ -461,11 +461,21 @@ def is_wsl() -> bool:
     
     
     def is_container() -> bool:
    -    """Return True when running inside a Docker/Podman container.
    +    """Return True when running inside a container.
     
    -    Checks ``/.dockerenv`` (Docker), ``/run/.containerenv`` (Podman),
    -    and ``/proc/1/cgroup`` for container runtime markers.  Result is
    -    cached for the process lifetime.  Import-safe — no heavy deps.
    +    Recognizes Docker (``/.dockerenv``), Podman (``/run/.containerenv``),
    +    and — via ``/proc/1/cgroup`` — the docker/podman/lxc cgroup-v1 markers.
    +
    +    cgroup v2 collapses ``/proc/1/cgroup`` to a single ``0::/`` line with no
    +    runtime marker, so containerd/CRI-O runtimes (the common case on
    +    Kubernetes/k3s) were previously missed. To cover those, also check:
    +      * ``KUBERNETES_SERVICE_HOST`` env var — set in every Kubernetes pod.
    +      * ``kubepods`` / ``containerd`` / ``crio`` markers in ``/proc/1/cgroup``.
    +      * the same markers in ``/proc/self/mountinfo`` (cgroup-v2 fallback).
    +
    +    Result is cached for the process lifetime.  Import-safe — no heavy deps.
    +
    +    See: NousResearch/hermes-agent#47111
         """
         global _container_detected
         if _container_detected is not None:
    ```

    ```diff
    @@ -476,10 +486,26 @@ def is_container() -> bool:
         if os.path.exists("/run/.containerenv"):
             _container_detected = True
             return True
    +    # Kubernetes always injects this into pod containers; absent on hosts.
    +    if os.environ.get("KUBERNETES_SERVICE_HOST"):
    +        _container_detected = True
    +        return True
    +    _CGROUP_MARKERS = ("docker", "podman", "/lxc/", "kubepods", "containerd", "crio")
         try:
             with open("/proc/1/cgroup", "r", encoding="utf-8") as f:
                 cgroup = f.read()
    -            if "docker" in cgroup or "podman" in cgroup or "/lxc/" in cgroup:
    +            if any(marker in cgroup for marker in _CGROUP_MARKERS):
    +                _container_detected = True
    +                return True
    +    except OSError:
    +        pass
    +    # cgroup v2: /proc/1/cgroup is just "0::/" with no marker. The container
    +    # runtime still shows up in the mount table (overlay rootfs, runtime mount
    +    # paths), so scan mountinfo as a last resort.
    +    try:
    +        with open("/proc/self/mountinfo", "r", encoding="utf-8") as f:
    +            mountinfo = f.read()
    +            if any(marker in mountinfo for marker in ("kubepods", "containerd", "crio")):
                     _container_detected = True
                     return True
         except OSError:
    ```
  - `tests/test_hermes_constants.py`

    ```diff
    @@ -139,13 +139,67 @@ def test_negative_case(self, monkeypatch, tmp_path):
             """Returns False on a regular Linux host."""
             import builtins
             self._reset_cache(monkeypatch)
    +        monkeypatch.delenv("KUBERNETES_SERVICE_HOST", raising=False)
             monkeypatch.setattr(os.path, "exists", lambda p: False)
             cgroup_file = tmp_path / "cgroup"
             cgroup_file.write_text("12:memory:/\n")
    +        mountinfo_file = tmp_path / "mountinfo"
    +        mountinfo_file.write_text("22 21 0:20 / /sys rw shared:7 - sysfs sysfs rw\n")
             _real_open = builtins.open
    -        monkeypatch.setattr("builtins.open", lambda p, *a, **kw: _real_open(str(cgroup_file), *a, **kw) if p == "/proc/1/cgroup" else _real_open(p, *a, **kw))
    +
    +        def _fake_open(p, *a, **kw):
    +            if p == "/proc/1/cgroup":
    +                return _real_open(str(cgroup_file), *a, **kw)
    +            if p == "/proc/self/mountinfo":
    +                return _real_open(str(mountinfo_file), *a, **kw)
    +            return _real_open(p, *a, **kw)
    +
    +        monkeypatch.setattr("builtins.open", _fake_open)
             assert is_container() is False
     
    +    def test_detects_kubernetes_env(self, monkeypatch):
    +        """KUBERNETES_SERVICE_HOST env var triggers detection (k8s/k3s pod)."""
    +        self._reset_cache(monkeypatch)
    +        monkeypatch.setattr(os.path, "exists", lambda p: False)
    +        monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "10.43.0.1")
    +        assert is_container() is True
    +
    +    def test_detects_cgroup_kubepods(self, monkeypatch, tmp_path):
    +        """/proc/1/cgroup containing 'kubepods' triggers detection."""
    +        import builtins
    +        self._reset_cache(monkeypatch)
    +        monkeypatch.delenv("KUBERNETES_SERVICE_HOST", raising=False)
    +        monkeypatch.setattr(os.path, "exists", lambda p: False)
    +        cgroup_file = tmp_path / "cgroup"
    +        cgroup_file.write_text("12:memory:/kubepods/besteffort/podabc\n")
    +        _real_open = builtins.open
    +        monkeypatch.setattr("builtins.open", lambda p, *a, **kw: _real_open(str(cgroup_file), *a, **kw) if p == "/proc/1/cgroup" else _real_open(p, *a, **kw))
    +        assert is_container() is True
    +
    +    def test_detects_cgroup_v2_via_mountinfo(self, monkeypatch, tmp_path):
    +        """cgroup v2 (0::/ only) falls back to containerd marker in mountinfo."""
    +        import builtins
    +        self._reset_cache(monkeypatch)
    +        monkeypatch.delenv("KUBERNETES_SERVICE_HOST", raising=False)
    +        monkeypatch.setattr(os.path, "exists", lambda p: False)
    +        cgroup_file = tmp_path / "cgroup"
    +        cgroup_file.write_text("0::/\n")  # cgroup v2 — no runtime marker
    +        mountinfo_file = tmp_path / "mountinfo"
    +        mountinfo_file.write_text(
    +            "1234 1233 0:42 /containerd/.../rootfs / rw - overlay overlay rw\n"
    +        )
    +        _real_open = builtins.open
    +
    +        def _fake_open(p, *a, **kw):
    +            if p == "/proc/1/cgroup":
    +                return _real_open(str(cgroup_file), *a, **kw)
    +            if p == "/proc/self/mountinfo":
    +                return _real_open(str(mountinfo_file), *a, **kw)
    +            return _real_open(p, *a, **kw)
    +
    +        monkeypatch.setattr("builtins.open", _fake_open)
    +        assert is_container() is True
    +
         def test_caches_result(self, monkeypatch):
             """Second call uses cached value without re-probing."""
             monkeypatch.setattr(hermes_constants, "_container_detected", True)
    ```
- **#48598: fix(constants): ignore host container runtime mounts**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#105351: fix(agent): a host that merely runs containers is no longer classified as one (#58135, #48594, salvage #58141)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_constants.py`, `tests/test_hermes_constants.py`
- **#117591: fix(constants): scratch dir honours HERMES_HOME_MODE and managed-home permissions instead of forcing 0700 (#117347, salvage #117359)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/config.py`, `hermes_constants.py`, `tests/hermes_cli/test_ensure_hermes_home_uid.py`, `tests/test_scratch_dir.py`
- **#44733: fix Kubernetes container detection**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `891c1d79c67c5240a8247a92adb2bb12c4f326e0`, `c5a3c0f8767a`, `#117591`, `#47131`
- **#44734: fix: Kubernetes containers**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#44733`, `#44733`, `#44721`, `#44733`
- **#44777: fix(constants): detect Kubernetes/k3s pods in is_container() (#44721)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#44733`
- **#47144: fix(constants): detect containerd + cgroup v2 in is_container() (#47111)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#47111`, `#47131`, `#47131`, `#47111`, `#46290`, `#47131`
- **#48695: fix(constants): exclude host Docker storage from is_container (#48594)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#58141: fix(agent): scan only root mount in is_container() cgroup-v2 fallback (#58135)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#105351`, `#48594`, `#105351`
- **#58145: fix(cli): stop is_container() false-positive on hosts running containers**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7b29f19146`, `#105351`, `#58141`
- **#65060: fix(constants): stop is_container() false positives on hosts that run containers (#65051)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7b29f19146`, `#105351`, `#58141`
- **#102169: fix(docker): don't classify a Docker host as a container**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#58141`, `#48594`

## Fix status

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