# NousResearch/hermes-agent · GatewayConfig missing dict-like .get() method bug

## Open now

- Nothing open on GitHub.

Multiple reports and fix attempts addressing an AttributeError caused by calling .get() on a GatewayConfig dataclass as if it were a dict, which crashed Telegram and Discord message handling. Proposed solutions include adding a .get() method, using getattr, or switching to attribute access.

## TL;DR

- 9 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #1287. Verify it addresses your case.
- **Recommended:** read PR #1287 (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-1287`. 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.20`: 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

- #973: \[Bug\]: GatewayConfig has no attribute 'get' when sending /start on Telegram
- #1158: fix(gateway): GatewayConfig.get() AttributeError crashes all Telegram/Discord message handling

## Prior & competing PRs

- **#1287: fix(gateway): avoid slash-command crash with GatewayConfig**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/config.py`, `gateway/run.py`, `tests/gateway/test_config.py`, `tests/test_quick_commands.py`
  - fix hunks:
  - `gateway/config.py`

    ```diff
    @@ -151,6 +151,9 @@ class GatewayConfig:
         
         # Reset trigger commands
         reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"])
    +
    +    # User-defined quick commands (slash commands that bypass the agent loop)
    +    quick_commands: Dict[str, Any] = field(default_factory=dict)
         
         # Storage paths
         sessions_dir: Path = field(default_factory=lambda: get_hermes_home() / "sessions")
    ```

    ```diff
    @@ -218,6 +221,7 @@ def to_dict(self) -> Dict[str, Any]:
                     p.value: v.to_dict() for p, v in self.reset_by_platform.items()
                 },
                 "reset_triggers": self.reset_triggers,
    +            "quick_commands": self.quick_commands,
                 "sessions_dir": str(self.sessions_dir),
                 "always_log_local": self.always_log_local,
             }
    ```

    ```diff
    @@ -252,12 +256,17 @@ def from_dict(cls, data: Dict[str, Any]) -> "GatewayConfig":
             if "sessions_dir" in data:
                 sessions_dir = Path(data["sessions_dir"])
             
    +        quick_commands = data.get("quick_commands", {})
    +        if not isinstance(quick_commands, dict):
    +            quick_commands = {}
    +
             return cls(
                 platforms=platforms,
                 default_reset_policy=default_policy,
                 reset_by_type=reset_by_type,
                 reset_by_platform=reset_by_platform,
                 reset_triggers=data.get("reset_triggers", ["/new", "/reset"]),
    +            quick_commands=quick_commands,
                 sessions_dir=sessions_dir,
                 always_log_local=data.get("always_log_local", True),
             )
    ```

    ```diff
    @@ -299,6 +308,16 @@ def load_gateway_config() -> GatewayConfig:
                 if sr and isinstance(sr, dict):
                     config.default_reset_policy = SessionResetPolicy.from_dict(sr)
     
    +            # Bridge quick commands from config.yaml into gateway runtime config.
    +            # config.yaml is the user-facing config source, so when present it
    +            # should override gateway.json for this setting.
    +            qc = yaml_cfg.get("quick_commands")
    +            if qc is not None:
    +                if isinstance(qc, dict):
    +                    config.quick_commands = qc
    +                else:
    +                    logger.warning("Ignoring invalid quick_commands in config.yaml (expected mapping, got %s)", type(qc).__name__)
    +
                 # Bridge discord settings from config.yaml to env vars
                 # (env vars take precedence — only set if not already defined)
                 discord_cfg = yaml_cfg.get("discord", {})
    ```
  - `gateway/run.py`

    ```diff
    @@ -1013,7 +1013,12 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
             
             # User-defined quick commands (bypass agent loop, no LLM call)
             if command:
    -            quick_commands = self.config.get("quick_commands", {})
    +            if isinstance(self.config, dict):
    +                quick_commands = self.config.get("quick_commands", {}) or {}
    +            else:
    +                quick_commands = getattr(self.config, "quick_commands", {}) or {}
    +            if not isinstance(quick_commands, dict):
    +                quick_commands = {}
                 if command in quick_commands:
                     qcmd = quick_commands[command]
                     if qcmd.get("type") == "exec":
    ```
  - `tests/gateway/test_config.py`

    ```diff
    @@ -6,6 +6,7 @@
         Platform,
         PlatformConfig,
         SessionResetPolicy,
    +    load_gateway_config,
     )
     
     
    ```

    ```diff
    @@ -89,15 +90,49 @@ def test_full_roundtrip(self):
                 platforms={
                     Platform.TELEGRAM: PlatformConfig(
                         enabled=True,
    -                    token="tok",
    +                    token="[redacted:secret-assignment]",
                         home_channel=HomeChannel(Platform.TELEGRAM, "123", "Home"),
                     ),
                 },
                 reset_triggers=["/new"],
    +            quick_commands={"limits": {"type": "exec", "command": "echo ok"}},
             )
             d = config.to_dict()
             restored = GatewayConfig.from_dict(d)
     
             assert Platform.TELEGRAM in restored.platforms
    -        assert restored.platforms[Platform.TELEGRAM].token == "tok"
    +        assert restored.platforms[Platform.TELEGRAM].token == "tok_123"
             assert restored.reset_triggers == ["/new"]
    +        assert restored.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}}
    +
    +
    +class TestLoadGatewayConfig:
    +    def test_bridges_quick_commands_from_config_yaml(self, tmp_path, monkeypatch):
    +        hermes_home = tmp_path / ".hermes"
    +        hermes_home.mkdir()
    +        config_path = hermes_home / "config.yaml"
    +        config_path.write_text(
    +            "quick_commands:\n"
    +            "  limits:\n"
    +            "    type: exec\n"
    +            "    command: echo ok\n",
    +            encoding="utf-8",
    +        )
    +
    +        monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +        config = load_gateway_config()
    +
    +        assert config.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}}
    +
    +    def test_invalid_quick_commands_in_config_yaml_are_ignored(self, tmp_path, monkeypatch):
    +        hermes_home = tmp_path / ".hermes"
    +        hermes_home.mkdir()
    +        config_path = hermes_home / "config.yaml"
    +        config_path.write_text("quick_commands: not-a-mapping\n", encoding="utf-8")
    +
    +        monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +        config = load_gateway_config()
    +
    +        assert config.quick_commands == {}
    ```
  - `tests/test_quick_commands.py`

    ```diff
    @@ -146,3 +146,20 @@ async def test_timeout_returns_error(self):
                 result = await runner._handle_message(event)
             assert result is not None
             assert "timed out" in result.lower()
    +
    +    @pytest.mark.asyncio
    +    async def test_gateway_config_object_supports_quick_commands(self):
    +        from gateway.config import GatewayConfig
    +        from gateway.run import GatewayRunner
    +
    +        runner = GatewayRunner.__new__(GatewayRunner)
    +        runner.config = GatewayConfig(
    +            quick_commands={"limits": {"type": "exec", "command": "echo ok"}}
    +        )
    +        runner._running_agents = {}
    +        runner._pending_messages = {}
    +        runner._is_user_authorized = MagicMock(return_value=True)
    +
    +        event = self._make_event("limits")
    +        result = await runner._handle_message(event)
    +        assert result == "ok"
    ```
- **#1364: fix: implement .get() method in GatewayConfig for dict-like access (#973)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#973`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#975: fix(gateway): avoid /start crash when config is a GatewayConfig**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#1287`
- **#1016: fix: GatewayConfig has no attribute 'get' when sending /start on Telegram (#973)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#1287`
- **#1070: Fix/gateway config get attribute error**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#1287`, `#973`
- **#1199: fix: use attribute access on GatewayConfig instead of dict .get()**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#1209: Fix: Add quick_commands field to GatewayConfig**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#1287`

## Fix status

- Merged to main in PR #1287 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-14`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `gateway`
