{"record":{"id":"aa4eacca2bf140c6","repo":"NousResearch/hermes-agent","slug":"iron-proxy-exited-immediately-code-proc-returnco","errorCode":null,"errorMessage":"iron-proxy exited immediately (code {proc.returncode}). Last log lines:\n{tail}","messagePattern":"iron-proxy exited immediately \\(code (.+?)\\)\\. Last log lines:\n(.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/proxy_sources/iron_proxy.py","lineNumber":1954,"sourceCode":"    install_handlers = (\n        platform.system() != \"Windows\"\n        and threading.current_thread() is threading.main_thread()\n    )\n    if install_handlers:\n        prev_sigint = signal.signal(signal.SIGINT, _interrupt_handler)\n        prev_sigterm = signal.signal(signal.SIGTERM, _interrupt_handler)\n    try:\n        deadline = time.time() + _STARTUP_GRACE_SECONDS\n        # Do-while shape: check listening at least once even when the\n        # grace window is 0 (test harness / synchronous fast-path).\n        while True:\n            if proc.poll() is not None:\n                tail = _tail_log(log_path, lines=20)\n                try:\n                    pidfile.unlink()\n                except FileNotFoundError:\n                    pass\n                raise RuntimeError(\n                    f\"iron-proxy exited immediately (code {proc.returncode}). \"\n                    f\"Last log lines:\\n{tail}\"\n                )\n            if _port_listening(probe_host, tunnel_port):\n                listening = True\n                break\n            if time.time() >= deadline:\n                break\n            time.sleep(0.1)\n    finally:\n        if install_handlers:\n            signal.signal(signal.SIGINT, prev_sigint)\n            signal.signal(signal.SIGTERM, prev_sigterm)\n\n    # Final exit check — process may have died right at deadline.\n    if proc.poll() is not None:\n        tail = _tail_log(log_path, lines=20)\n        try:","sourceCodeStart":1936,"sourceCodeEnd":1972,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/proxy_sources/iron_proxy.py#L1936-L1972","documentation":"During the startup grace poll loop, proc.poll() returned non-None before the tunnel port ever listened — the iron-proxy process died immediately. The pidfile (written right after Popen) is unlinked and the last 20 log lines are attached so the config/runtime failure is visible. This is the 'died early' branch inside the do-while listening check.","triggerScenarios":"start_proxy() where the proxy binary rejects its config (bad YAML, unknown fields), cannot bind its port, cannot read a referenced cert/key file, or crashes on startup — all within _STARTUP_GRACE_SECONDS.","commonSituations":"Hand-edited config.yaml with a syntax or schema error; port already in use by another service; TLS key/cert paths wrong; a proxy version whose config schema changed after an upgrade; missing capabilities for binding a low port.","solutions":["Read the 'Last log lines' in the message — the proxy's own error is almost always the first line there","Fix the reported config problem in the generated config / config.yaml and retry `hermes egress start`","If the log shows 'address already in use', free the port or change tunnel_port, then retry","If the schema changed after an upgrade, re-run `hermes egress setup` to regenerate the config"],"exampleFix":"# before: config references a missing cert\n# RuntimeError: iron-proxy exited immediately (code 1). Last log lines:\n#   error: cannot open cert file /etc/ssl/wrong.pem\n# fix path in config, then:\nhermes egress start\n\n# after: proxy stays alive through the grace window and binds the port","handlingStrategy":"try-catch","validationCode":"import yaml\nfrom pathlib import Path\n\ndef config_parses(cfg: Path) -> bool:\n    try:\n        yaml.safe_load(cfg.read_text())\n        return True\n    except yaml.YAMLError:\n        return False\n\n# and pre-check the port is free:\nimport socket\n\ndef port_free(host: str, port: int) -> bool:\n    with socket.socket() as s:\n        return s.connect_ex((host, port)) != 0","typeGuard":null,"tryCatchPattern":"try:\n    start_proxy(...)\nexcept RuntimeError as e:\n    if 'exited immediately' in str(e):\n        tail = str(e).split('Last log lines:', 1)[-1]\n        diagnose_and_fix(tail)  # fix config/port/cert, then retry once","preventionTips":["Never hand-edit the generated proxy config without validating YAML first","Pre-flight the tunnel port with a socket connect before start","After upgrading the proxy binary, regenerate config via setup (schema drift)"],"tags":["egress-proxy","startup","configuration"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}