Skip to content

Use Hermes hook payloads for rich notifications - #4851

Merged
lawrencecchen merged 18 commits into
mainfrom
feat-hermes-rich-notifications
May 29, 2026
Merged

lawrencecchen merged 18 commits into
mainfrom
feat-hermes-rich-notifications

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Read Hermes hook serialization and use extra.assistant_response for completion notifications.
  • Install Hermes pre_approval_request as a normal notification hook and render description plus command for approval requests.
  • Coalesce duplicate Hermes event names in config.yaml so notification and Feed hooks share one event block.

Verification

  • git diff --check HEAD~2..HEAD
  • swift test --package-path Packages/CMUXAgentLaunch
  • ./scripts/reload.sh --tag hermnotif
  • Tagged CLI smoke: Hermes completion notification used extra.assistant_response.
  • Tagged CLI smoke: Hermes approval notification rendered recursive delete: rm -rf build and marked needs input.
  • Tagged install smoke: pre_approval_request_count=1, notification_hook_count=1, feed_hook_count=1.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches agent hook handling, session store, resume shell commands, and environment allowlisting; mistakes could mis-notify users or break Hermes resume against custom Codex endpoints.

Overview
Hermes hook integration now surfaces richer UI notifications and approval lifecycle handling. Completion messages read extra.assistant_response (and related fields); pre_approval_request maps to notification hooks with localized description + command text; a new approval-response path clears stored notification state, restores Running status, and republishes resume bindings.

Hermes + Codex subrouter resume is added via HermesAgentCodexEnvironment: derive HERMES_CODEX_BASE_URL / CUSTOM_BASE_URL from Codex config.toml, allowlist them only for hermes-agent, rewrite stale openai-codex to custom, and prepend guarded hermes config set bootstrap on hook/surface resume when appropriate. Hook YAML install coalesces multiple cmux hooks under one event name; feed telemetry is suppressed for Hermes events that already have dedicated feed hooks to avoid duplicates.

Reviewed by Cursor Bugbot for commit e8e1613. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Richer Hermes notifications now read hook extra payloads and show clear approval prompts. Hermes resumes with Codex subrouter defaults via a guarded bootstrap that preserves explicit provider settings, and the Codex environment API is documented.

  • New Features

    • Notifications: use extra.assistant_response; render approvals as "description: command"; add approval-response to clear prompts and restore Running.
    • Hermes Codex defaults and API docs: derive and allowlist HERMES_CODEX_BASE_URL/CUSTOM_BASE_URL; prepend guarded hermes config set on resume; rewrite openai-codex to custom; coalesce multiple hooks under one Hermes event.
  • Bug Fixes

    • Bootstrap safety: skip when model.api_mode is set or provider isn’t custom/openai-codex; dedupe and handle malformed commands; keep explicit subrouter URLs; keep bootstrap behind a CWD guard.
    • Provider/env hygiene: preserve explicit --provider; scope env capture to Hermes; generic terminals don’t derive Codex defaults.
    • Parsing/telemetry: include nested extra fields; suppress duplicate feed events when Hermes notifications also install feed hooks.

Written for commit e8e1613. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Hermes agent: pre-approval-request notifications and approval-response handling; Hermes-aware resume flow that bootstraps provider/Codex defaults and preserves resume environment.
  • Bug Fixes

    • Improved hook payload parsing to recognize alias fields and extract assistant messages reliably.
    • Environment handling now derives and preserves Codex/custom base URLs and sanitizes preserved launch args.
    • Hook installation now coalesces multiple events under a single named section.
  • Tests

    • Added/updated tests covering Hermes notifications, Codex config parsing, startup env derivation, hook coalescing, and resume behavior.
  • Documentation

    • Added localization entry for Hermes approval notification body.

Review Change Stack

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented May 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled May 29, 2026 10:31am
cmux-staging Building Building Preview May 29, 2026 10:31am

@coderabbitai

coderabbitai Bot commented May 27, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Integrates Hermes Codex discovery and provider normalization with agent hooks: groups Hermes hook events, expands CLI hook payload normalization (approval fast-path), makes resume-command pipeline Hermes-aware (env, provider, bootstrap), adjusts sanitizer/environment policy, and adds tests plus localization.

Changes

Hermes Codex Configuration and Approval Notification Integration

Layer / File(s) Summary
Hook definitions and Hermes event grouping
CLI/CMUXCLI+AgentHookDefinitions.swift, Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift, Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentHookConfigTests.swift
Adds pre_approval_request → notification and post_approval_response → approval-response hook mappings; refactors hook YAML installation to group events by name, preserving order while coalescing multiple events under a single YAML key.
Hermes Codex environment discovery and TOML parsing
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift
Introduces HermesAgentCodexEnvironment enum with provider constants, Codex config path resolution (respecting CODEX_HOME, otherwise ~/.codex), minimal TOML parsing (with comment/quote handling), URL normalization/validation, and helpers for extracting default base URL, custom base URL, and model from config.
Environment policy, sanitizer, and provider handling
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift, Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift, Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
Extends environment policy allowlist with CUSTOM_BASE_URL and HERMES_CODEX_BASE_URL; makes selectedEnvironment kind-aware to filter Hermes keys only for hermes-agent; updates Hermes sanitizer to replace openai-codex provider with custom in preserved arguments; adds test coverage for provider preservation and rewriting.
Hermes index resume provider and startup environment defaults
Sources/HermesAgentIndex.swift, Sources/TerminalStartupEnvironment.swift
Adds --provider to Hermes resume command using HermesAgentCodexEnvironment.defaultProvider; applies default Codex base URL via applyingDefaultCodexBaseURL in merged startup environment.
Workspace resume binding transforms and Hermes bootstrap logic
Sources/Workspace.swift
Introduces surfaceResumeBindingForStartup helper; adds hermesAgentSubrouterBindingForStartup to transform hermes-agent bindings by applying Codex defaults, rewriting provider, and conditionally prepending Hermes bootstrap config-set commands; persists effective per-panel resume binding with fallback.
CLI hook payload normalization, approval fast-path, and hermes-agent resume wrapping
CLI/cmux.swift
Extends hook payload compactification to include assistant_response in extra; refactors assistant-message extraction to search both top-level and extra fields; adds Hermes pre_approval_request fast-path deriving approval message from extra fields; threads resumeEnvironment through resume-command generation; wraps hermes-agent resume with conditional Hermes bootstrap config steps; adds kind-aware selectedAgentLaunchEnvironment; introduces Hermes feed telemetry suppression; updates approval-response path to call markNotificationResolved(..., runtimeStatus: .running).
Test coverage: hook persistence, environment derivation, snapshots, and localization
cmuxTests/CLIGenericHookPersistenceTests.swift, cmuxTests/GhosttyTerminalStartupEnvironmentTests.swift, Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentCodexEnvironmentTests.swift, cmuxTests/RestorableAgentHookProviderHermesTests.swift, cmuxTests/SessionPersistenceTests.swift, Resources/Localizable.xcstrings
Adds Hermes notification persistence test using extra payload fields; Ghostty startup env Codex config derivation test; comprehensive HermesAgentCodexEnvironment TOML/URL parsing and policy selection tests; Hermes resume-command snapshots with base-URL preservation; session persistence bootstrap verification; new localization key for approval-command notification body.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#4237: Builds Hermes-specific behavior on top of the per-surface SurfaceResumeBindingSnapshot/surface-resume binding restore pipeline introduced in that PR.
  • manaflow-ai/cmux#4777: Modifies Sources/Workspace.swift resume restoration flow; overlaps with this PR's changes to resume binding resolution and launcher-script/startup wiring.

Poem

🐰 A Hermes-agent learns to speak with Codex,
Config files whisper secrets in TOML lines,
Approvals bloom as notification hooks sing,
Providers rewrite and resume commands tidy—
Hops of code, neatly wrapped, and all in time. ✨


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error CLI/cmux.swift adds +320 lines to an oversized file (31,251 lines), exceeding the 250-line limit. No extraction exception met: file grows rather than shrinks. Extract Hermes hook handling, notification normalization, and resume bootstrap into a new CMUXAgentLaunch target, shrinking cmux.swift by >200 lines to satisfy extraction exception.
Cmux User-Facing Error Privacy ❌ Error Hermes hook payload fields (command, description) are exposed directly in user notifications without sanitization, violating the upstream vendor data privacy rule. Redact or sanitize Hermes command and description values before displaying in user-facing notifications.
Cmux Full Internationalization ❌ Error New localization key agent.hermes.notification.body.approvalCommand in Resources/Localizable.xcstrings has only en/ja translations (2/20 locales) but requires all 20 supported locales. Add translated entries for all 20 supported locales (ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant) in the agent.hermes.notification.body.approvalCommand string catalog entry.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Stateless enum helpers use only static functions with value-type parameters. Sendable structs are immutable. No actor isolation violations detected.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no new blocking/timing synchronization; new code adds configuration and string processing only.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code and localization resources. The rule explicitly excludes Swift, covering only TypeScript, JavaScript, shell, and non-Swift build/runtime scripts.
Cmux Algorithmic Complexity ✅ Passed No algorithmic complexity violations. Config file read once, linearly scanned. Hook grouping is dictionary-based. Resume processing per-binding. Bounded collections throughout.
Cmux Swift Concurrency ✅ Passed All new code (HermesAgentCodexEnvironment.swift, Workspace.swift helpers, CLI handlers) is purely synchronous with no DispatchQueue.global(), Combine, @escaping, or problematic Tasks.
Cmux Swift @Concurrent ✅ Passed All changes are synchronous. File I/O in HermesAgentCodexEnvironment only executes in test-only code path (applyHermesCodexDefaults: true). No async/nonisolated violations.
Cmux Swift Logging ✅ Passed No logging violations detected. PR adds 0 print/debugPrint/NSLog calls in production code; existing NSLog in Workspace.swift are debug-only (#if DEBUG guards), and CLI output via print is allowed.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI state layout violations. Changes are to backend agent launch, hook config, and non-SwiftUI helper functions; no new @Published/@Observable/@StateObject state introduced.
Cmux Architecture Rethink ✅ Passed No timing dependencies, locks, observers, or split lifecycle ownership added. New bootstrap logic uses immutable snapshots in nonisolated static methods.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code added. Changes focus on Hermes hooks, agent launch environments, and Codex configuration only.
Title check ✅ Passed The pull request title 'Use Hermes hook payloads for rich notifications' clearly summarizes the main change: leveraging Hermes hook extra payloads to enable richer notification rendering and handling.
Description check ✅ Passed PR description covers key changes (Hermes hook payloads, notification rendering, resume bootstrapping), testing performed (git diff, swift tests, smoke tests), and a detailed verification section.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-hermes-rich-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires Hermes hook payloads into rich notifications by reading extra.assistant_response for completion messages and generating approval notifications from extra.description/extra.command. It also adds a new approval-response action that clears the permission UI and restores Running state, prepends a guarded hermes config set bootstrap on resume (both CLI and workspace paths), coalesces duplicate event names in hook config YAML, and fixes Hermes-specific env-var scoping.

  • Notifications: claudeAssistantMessageFromHookPayload now falls through to extra keys; hermesAgentApprovalNotificationMessage formats approval prompts; the new .approvalResponse action calls markNotificationResolved and sends clear_notifications.
  • Resume bootstrap: both hermesAgentSubrouterResumeCommand (CLI) and hermesAgentSubrouterBindingForStartup (Workspace) prepend hermes config set model.{provider,base_url,api_mode[,default]} after the CWD guard, stripping any previously baked-in bootstrap first to stay idempotent.
  • Env scoping: AgentLaunchEnvironmentPolicy.selectedEnvironment(kind:) now strips CUSTOM_BASE_URL/HERMES_CODEX_BASE_URL for non-Hermes agents; TerminalStartupEnvironment.mergedStartupEnvironment adds an opt-in applyHermesCodexDefaults flag that defaults to false for generic surfaces.

Confidence Score: 5/5

Safe to merge. The approval notification and resume bootstrap paths are well-guarded, idempotent, and covered by integration tests. The env-key scoping fix correctly addresses the prior review concern.

All changed paths have test coverage, the bootstrap insertion/removal is idempotent, and the env-leakage fixes are properly gated on agent kind. The two findings are style-level observations about code placement and duplication, not behavioral defects.

No files require special attention. The Workspace.swift additions work correctly; the package boundary concern is a future cleanup opportunity rather than a current risk.

Important Files Changed

Filename Overview
CLI/cmux.swift +351 lines: adds markNotificationResolved, approvalResponse case, hermesAgentSubrouterResumeCommand bootstrap prepend, provider-rewrite and bootstrap-guard helpers, extra-field payload extraction, feed-telemetry suppression. Logic is correct; private provider-rewrite helper duplicates package-level function.
Sources/Workspace.swift +357 lines: hermesAgentSubrouterBindingForStartup, custom surfaceResumeShellWords parser, bootstrap insertion/removal, provider-rewrite, and CWD-guard helpers. Logic is sound and idempotent; independently testable shell-parsing logic is added to an already oversized file instead of a package.
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift New 346-line package file; reads Codex config.toml for HERMES_CODEX_BASE_URL / CUSTOM_BASE_URL derivation, TOML string parser, URL normalisation. Well-scoped and tested.
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift Adds kind parameter to selectedEnvironment; strips Hermes-only keys for non-hermes agents. Correctly fixes env leakage.
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift Event coalescing via EventGroup; groups multiple cmux commands under the same Hermes event name in config YAML. Logic is clean and tested.
CLI/CMUXCLI+AgentHookDefinitions.swift Adds approvalResponse action and maps pre_approval_request / post_approval_response to notification / approval-response subcommands. Straightforward.
Sources/TerminalStartupEnvironment.swift Adds opt-in applyHermesCodexDefaults flag (default false) to mergedStartupEnvironment; generic terminals unchanged. Addresses prior env-leakage concern.
Resources/Localizable.xcstrings Adds agent.hermes.notification.body.approvalCommand with en + ja translations, consistent with all other agent.* entries in this catalog.

Sequence Diagram

sequenceDiagram
    participant Hermes as Hermes Agent
    participant CLI as cmux CLI hook
    participant Store as ClaudeHookSessionStore
    participant Surface as cmux Surface

    Note over Hermes,Surface: Approval request flow
    Hermes->>CLI: pre_approval_request (notification subcommand)
    CLI->>Store: update session (needsInput, lastSubtitle, lastBody)
    CLI->>Surface: notify_target_async description: command
    CLI->>Surface: set_status needs input

    Note over Hermes,Surface: Approval response flow
    Hermes->>CLI: post_approval_response (approval-response subcommand)
    CLI->>Store: markNotificationResolved (running, clear subtitle/body)
    CLI->>Surface: clear_notifications
    CLI->>Surface: set_status Running
    CLI->>Surface: surface.resume.set

    Note over Hermes,Surface: Completion notification flow
    Hermes->>CLI: post_llm_call (agent-response subcommand)
    CLI->>Store: update session (idle)
    CLI->>Surface: notify_target_async with assistant_response
    CLI->>Surface: set_status Idle
Loading

Reviews (16): Last reviewed commit: "docs: document hermes codex environment ..." | Re-trigger Greptile

Comment thread CLI/cmux.swift
Comment on lines +22363 to +22381
private func hermesAgentApprovalNotificationMessage(def: AgentHookDef, object: [String: Any]) -> String? {
guard def.name == "hermes-agent" else { return nil }
let event = firstString(in: object, keys: ["hook_event_name", "hookEventName", "event", "event_name"])
guard event == "pre_approval_request" else { return nil }
let extra = (object["extra"] as? [String: Any]) ?? [:]
let command = firstString(in: extra, keys: ["command"])
let description = firstString(in: extra, keys: ["description", "pattern_key", "patternKey"])

switch (description, command) {
case let (description?, command?):
return "\(description): \(command)"
case let (description?, nil):
return description
case let (nil, command?):
return command
default:
return nil
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded : separator in user-visible notification body

"\(description): \(command)" assembles the notification body with a raw English colon-space separator rather than a localized format string. Every other user-visible format string in this file routes through String.localizedStringWithFormat(String(localized:...)). For locales that place the command before the description, or that use different punctuation, the concatenation order and separator should be in a string-catalog entry (e.g., "%1$@: %2$@" with a defaultValue: and translated variants).

Rule Used: Flag production user-facing text that is not fully... (source)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 20751-20765: The compaction loop in CLI/cmux.swift builds
compactExtra from a fixed key list but is missing the new extraction keys, so
fields like "assistantPreamble", "assistant_preamble", and "title" get dropped;
update the key array used in the for-loop that calls compactClaudeHookValue (the
array feeding compactExtra) to include "assistantPreamble",
"assistant_preamble", and "title" so compactExtra preserves those values and
compact["extra"] retains notification/assistant text across persistence.

In
`@Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift`:
- Around line 53-63: The function codexBaseURL(fromChatGPTBaseURL:) currently
does raw string rewriting which can produce invalid URLs (e.g. "https://" →
"https:/codex"); instead parse the input with URLComponents (or URL) after
trimming, verify scheme is "http"/"https" and that host is non-empty, and reject
(return nil) if parsing fails or if query/fragment are present; then normalize
the path by removing trailing slashes, check if the path already ends with
"/codex" and otherwise append "/codex", rebuild a valid URL string from the
components (scheme, host, normalized path) and return it—update references to
trimmed/withoutTrailingSlash logic inside codexBaseURL(fromChatGPTBaseURL:)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e399ad8b-45a4-4b4d-a3a6-223c1b0457ea

📥 Commits

Reviewing files that changed from the base of the PR and between 1c7d079 and 9239a40.

📒 Files selected for processing (14)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentCodexEnvironmentTests.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentHookConfigTests.swift
  • Sources/HermesAgentIndex.swift
  • Sources/TerminalStartupEnvironment.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift
  • cmuxTests/GhosttyTerminalStartupEnvironmentTests.swift
  • cmuxTests/RestorableAgentHookProviderHermesTests.swift

Comment thread CLI/cmux.swift
Comment on lines +83 to +86
merged = HermesAgentCodexEnvironment.applyingDefaultCodexBaseURL(
to: merged,
ambientEnvironment: ambientEnvironment
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 CUSTOM_BASE_URL injected into every terminal's startup environment

HermesAgentCodexEnvironment.applyingDefaultCodexBaseURL is now called unconditionally in mergedStartupEnvironment, which runs for every terminal surface — not just Hermes terminals. When a user has a Codex config with a non-public openai_base_url (e.g. an internal subrouter), CUSTOM_BASE_URL is silently set in the initial shell environment of Claude Code, Copilot, OpenCode, and every other agent launched via cmux.

Compounding this, CUSTOM_BASE_URL was added to AgentLaunchEnvironmentPolicy.safeEnvironmentKeys without any agent-kind guard, so selectedAgentLaunchEnvironment will capture and persist it for all agents' resume records — not just Hermes. Any agent that reads CUSTOM_BASE_URL from its environment will be rerouted to the Codex subrouter. The launch-capture path in cmux.swift is correctly gated on kind == "hermes-agent", so the fix here should mirror that pattern: only inject from mergedStartupEnvironment (or guard CUSTOM_BASE_URL in safeEnvironmentKeys) when the surface is Hermes-specific.

Comment thread Sources/Workspace.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift (1)

116-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate chatgpt_base_url before appending /codex.

Raw prefix checks plus string concatenation still turn malformed values like https:// or https://host?x=1 into invalid HERMES_CODEX_BASE_URL values. This helper is the normalization boundary, so it should reject anything without a real host and only mutate a parsed path.

Suggested fix
 public static func codexBaseURL(fromChatGPTBaseURL rawValue: String) -> String? {
     let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
-    guard trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") else {
+    guard var components = URLComponents(string: trimmed),
+          let scheme = components.scheme?.lowercased(),
+          ["http", "https"].contains(scheme),
+          components.host?.isEmpty == false,
+          components.query == nil,
+          components.fragment == nil else {
         return nil
     }
-    let withoutTrailingSlash = trimmed.replacingOccurrences(of: "/+$", with: "", options: .regularExpression)
-    guard !withoutTrailingSlash.isEmpty else { return nil }
-    if withoutTrailingSlash.lowercased().hasSuffix("/codex") {
-        return withoutTrailingSlash
-    }
-    return withoutTrailingSlash + "/codex"
+    let normalizedPath = components.path
+        .replacingOccurrences(of: "/+$", with: "", options: .regularExpression)
+    components.path = normalizedPath.lowercased().hasSuffix("/codex")
+        ? normalizedPath
+        : normalizedPath + "/codex"
+    return components.string
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift`
around lines 116 - 126, The codexBaseURL(fromChatGPTBaseURL:) helper currently
only trims and checks prefixes then appends "/codex", which accepts malformed
inputs like "https://" or "https://host?x=1"; instead parse the trimmed string
into a URL or URLComponents, verify scheme is "http" or "https" and that host is
non-empty (and reject if host missing), strip trailing slashes from the path
portion only, preserve any valid path, ensure there is no query/fragment (or
reject if present per caller expectations), and then if the resulting path does
not already end with "/codex" append "/codex" and return the reconstructed URL
string; locate and update codexBaseURL(fromChatGPTBaseURL:) to use
URL/URLComponents validation and reconstruction rather than raw string prefix
checks and concatenation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 23339-23340: The provider-rewrite is happening after the command
is fully shell-quoted, causing accidental rewrites inside user payloads; change
hermesAgentCommandByReplacingOpenAICodexProvider to operate on the argv token
array before calling cliShellQuote (i.e. find/replace any "--provider
openai-codex" token or the "--provider" token followed by "openai-codex" and
replace just those tokens), and update any checks that use
contains("model.api_mode") to inspect the argv tokens instead of the joined
command string (apply same fix where hermesAgentSubrouterResumeCommand
constructs argv and where similar logic appears around lines referenced in the
comment). Ensure only provider flag tokens are mutated, not arbitrary substrings
of other arguments.

In
`@Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift`:
- Around line 129-137: customBaseURL currently forwards malformed inputs like
"https://" or "https://host?x=1"; update the validation in
customBaseURL(fromOpenAIBaseURL:) to parse the trimmed string with URL(string:)
and reject any URL that lacks a non-empty host or that contains query or
fragment components (i.e. require url.scheme == "http" or "https", url.host !=
nil, url.query == nil, url.fragment == nil), then continue to strip trailing
slashes and run the existing hostMatches check (hostMatches(..., hostSuffix:
"api.openai.com")); return nil on any of these failures so only well-formed base
URLs are returned.

In `@Sources/Workspace.swift`:
- Around line 855-913: The Hermes resume-rewrite logic
(hermesAgentSubrouterBindingForStartup,
hermesAgentCommandByReplacingOpenAICodexProvider, normalizedSurfaceResumeValue,
surfaceResumeShellQuote) should be extracted out of Workspace.swift into the
Hermes launch utilities module so it can be unit-tested independently; move
these functions into a new utility file/class (e.g., HermesLaunchUtils or
HermesAgentCodexHelpers), keep their visibility appropriate (nonisolated/private
-> internal or fileprivate as needed for tests), update Workspace.swift to call
the new helpers, and ensure any referenced types like
HermesAgentCodexEnvironment and SurfaceResumeBindingSnapshot are
imported/visible to the new file and covered by unit tests.
- Around line 863-873: The normalized base URL returned by
normalizedSurfaceResumeValue is not written back into the environment, so
surrounding whitespace can still persist; update the environment dictionary at
HermesAgentCodexEnvironment.customBaseURLEnvironmentKey with the
trimmed/normalized string before assigning result.environment and returning
binding, i.e. capture the normalized value, set
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey] =
normalizedValue (or remove the key if normalizedValue is nil/empty) and then
proceed to set result.environment and result.command using
hermesAgentCommandByReplacingOpenAICodexProvider.

---

Duplicate comments:
In
`@Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift`:
- Around line 116-126: The codexBaseURL(fromChatGPTBaseURL:) helper currently
only trims and checks prefixes then appends "/codex", which accepts malformed
inputs like "https://" or "https://host?x=1"; instead parse the trimmed string
into a URL or URLComponents, verify scheme is "http" or "https" and that host is
non-empty (and reject if host missing), strip trailing slashes from the path
portion only, preserve any valid path, ensure there is no query/fragment (or
reject if present per caller expectations), and then if the resulting path does
not already end with "/codex" append "/codex" and return the reconstructed URL
string; locate and update codexBaseURL(fromChatGPTBaseURL:) to use
URL/URLComponents validation and reconstruction rather than raw string prefix
checks and concatenation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 461cdce3-ead4-49d8-b9d1-2a031b9843f5

📥 Commits

Reviewing files that changed from the base of the PR and between 9239a40 and 29cbfd4.

📒 Files selected for processing (9)
  • CLI/cmux.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentCodexEnvironmentTests.swift
  • Sources/Workspace.swift
  • cmuxTests/GhosttyTerminalStartupEnvironmentTests.swift
  • cmuxTests/RestorableAgentHookProviderHermesTests.swift
  • cmuxTests/SessionPersistenceTests.swift

Comment thread CLI/cmux.swift Outdated
Comment thread Sources/Workspace.swift
Comment on lines +855 to 913
nonisolated private static func hermesAgentSubrouterBindingForStartup(
_ binding: SurfaceResumeBindingSnapshot
) -> SurfaceResumeBindingSnapshot {
guard binding.source == "agent-hook",
binding.kind == "hermes-agent" else {
return binding
}

var environment = binding.environment ?? [:]
environment = HermesAgentCodexEnvironment.applyingDefaultCodexBaseURL(to: environment)
guard let baseURL = normalizedSurfaceResumeValue(
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey]
) else {
return binding
}

var result = binding
result.environment = environment.isEmpty ? nil : environment
result.command = hermesAgentCommandByReplacingOpenAICodexProvider(result.command)
guard !result.command.contains("model.api_mode") else {
return result
}

var bootstrap = [
"hermes config set model.provider \(surfaceResumeShellQuote(HermesAgentCodexEnvironment.defaultProvider)) >/dev/null",
"hermes config set model.base_url \(surfaceResumeShellQuote(baseURL)) >/dev/null",
"hermes config set model.api_mode \(surfaceResumeShellQuote(HermesAgentCodexEnvironment.codexResponsesAPIMode)) >/dev/null"
]
if let model = HermesAgentCodexEnvironment.defaultCodexModel(environment: environment) {
bootstrap.append("hermes config set model.default \(surfaceResumeShellQuote(model)) >/dev/null")
}
result.command = bootstrap.joined(separator: " && ") + " && " + result.command
return result
}

nonisolated private static func hermesAgentCommandByReplacingOpenAICodexProvider(_ command: String) -> String {
var result = command
let replacements = [
("'--provider' 'openai-codex'", "'--provider' 'custom'"),
("\"--provider\" \"openai-codex\"", "\"--provider\" \"custom\""),
("--provider openai-codex", "--provider custom"),
("'--provider=openai-codex'", "'--provider=custom'"),
("\"--provider=openai-codex\"", "\"--provider=custom\""),
("--provider=openai-codex", "--provider=custom")
]
for (old, new) in replacements {
result = result.replacingOccurrences(of: old, with: new)
}
return result
}

nonisolated private static func normalizedSurfaceResumeValue(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed?.isEmpty == false ? trimmed : nil
}

nonisolated private static func surfaceResumeShellQuote(_ value: String) -> String {
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move this Hermes resume-rewrite logic out of Workspace.swift.

This block is pure command/env transformation and shell quoting, so it does not need to live in the app target’s root Sources/ path. Please move it alongside the Hermes launch utilities so it can be unit-tested without Workspace/UI coupling.

As per coding guidelines, "Do not implement feature logic directly in the app target/module's root Sources/ path when the logic is independent of cmux app lifecycle and can compile/test without AppKit, SwiftUI view state, Ghostty globals, or process-wide singletons."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 855 - 913, The Hermes resume-rewrite
logic (hermesAgentSubrouterBindingForStartup,
hermesAgentCommandByReplacingOpenAICodexProvider, normalizedSurfaceResumeValue,
surfaceResumeShellQuote) should be extracted out of Workspace.swift into the
Hermes launch utilities module so it can be unit-tested independently; move
these functions into a new utility file/class (e.g., HermesLaunchUtils or
HermesAgentCodexHelpers), keep their visibility appropriate (nonisolated/private
-> internal or fileprivate as needed for tests), update Workspace.swift to call
the new helpers, and ensure any referenced types like
HermesAgentCodexEnvironment and SurfaceResumeBindingSnapshot are
imported/visible to the new file and covered by unit tests.

Comment thread Sources/Workspace.swift
Comment on lines +863 to +873
var environment = binding.environment ?? [:]
environment = HermesAgentCodexEnvironment.applyingDefaultCodexBaseURL(to: environment)
guard let baseURL = normalizedSurfaceResumeValue(
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey]
) else {
return binding
}

var result = binding
result.environment = environment.isEmpty ? nil : environment
result.command = hermesAgentCommandByReplacingOpenAICodexProvider(result.command)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Persist the normalized base URL back into environment.

normalizedSurfaceResumeValue(...) trims the URL for the bootstrap commands, but result.environment still carries the original untrimmed value. If Hermes reads the env var during resume, a value with surrounding whitespace can still override the corrected config.

💡 Suggested fix
         guard let baseURL = normalizedSurfaceResumeValue(
             environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey]
         ) else {
             return binding
         }
+        environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey] = baseURL
 
         var result = binding
         result.environment = environment.isEmpty ? nil : environment
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var environment = binding.environment ?? [:]
environment = HermesAgentCodexEnvironment.applyingDefaultCodexBaseURL(to: environment)
guard let baseURL = normalizedSurfaceResumeValue(
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey]
) else {
return binding
}
var result = binding
result.environment = environment.isEmpty ? nil : environment
result.command = hermesAgentCommandByReplacingOpenAICodexProvider(result.command)
var environment = binding.environment ?? [:]
environment = HermesAgentCodexEnvironment.applyingDefaultCodexBaseURL(to: environment)
guard let baseURL = normalizedSurfaceResumeValue(
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey]
) else {
return binding
}
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey] = baseURL
var result = binding
result.environment = environment.isEmpty ? nil : environment
result.command = hermesAgentCommandByReplacingOpenAICodexProvider(result.command)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 863 - 873, The normalized base URL
returned by normalizedSurfaceResumeValue is not written back into the
environment, so surrounding whitespace can still persist; update the environment
dictionary at HermesAgentCodexEnvironment.customBaseURLEnvironmentKey with the
trimmed/normalized string before assigning result.environment and returning
binding, i.e. capture the normalized value, set
environment[HermesAgentCodexEnvironment.customBaseURLEnvironmentKey] =
normalizedValue (or remove the key if normalizedValue is nil/empty) and then
proceed to set result.environment and result.command using
hermesAgentCommandByReplacingOpenAICodexProvider.

…fications

# Conflicts:
#	CLI/cmux.swift
#	Sources/Workspace.swift
#	cmuxTests/RestorableAgentHookProviderHermesTests.swift
Comment thread Sources/Workspace.swift
Comment thread CLI/cmux.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0784208. Configure here.

Comment thread CLI/cmux.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentCodexEnvironmentTests.swift`:
- Around line 118-126: The assertion that
AgentLaunchEnvironmentPolicy.selectedEnvironment(..., kind: "codex").isEmpty
should be split into its own unit test: keep the existing test that verifies
allowed Hermes Codex URLs focused on the positive case (allowing URLs) and move
the blocking assertion into a new test named e.g.
testBlockingHermesCodexEnvironment_whenCustomBaseUrlConflicts; locate the call
to AgentLaunchEnvironmentPolicy.selectedEnvironment and the `#expect`(...)
assertion in HermesAgentCodexEnvironmentTests.swift, create a separate test
function that only checks the .isEmpty result for kind: "codex", and ensure each
test has a clear descriptive name and asserts a single scenario.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81c188f2-4ebb-4c18-afc1-10221118720a

📥 Commits

Reviewing files that changed from the base of the PR and between 29cbfd4 and e8e1613.

📒 Files selected for processing (8)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift
  • Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentCodexEnvironment.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
  • Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentCodexEnvironmentTests.swift
  • Resources/Localizable.xcstrings
💤 Files with no reviewable changes (1)
  • Resources/Localizable.xcstrings

Comment on lines +118 to +126
#expect(
AgentLaunchEnvironmentPolicy.selectedEnvironment(
from: [
"CUSTOM_BASE_URL": "http://subrouter-team:31415/v1",
"HERMES_CODEX_BASE_URL": "http://subrouter-team:31415/backend-api/codex",
],
kind: "codex"
).isEmpty
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider splitting this assertion into a separate test.

The test name describes allowing URLs for Hermes Codex, but this assertion verifies the opposite behavior (blocking for kind: "codex"). Splitting into a separate test would improve clarity and follow the one-scenario-per-test pattern.

♻️ Proposed refactor
     `@Test`("Allows Hermes Codex subrouter URLs in captured launch environment")
     func allowsHermesCodexSubrouterURLsInCapturedLaunchEnvironment() {
         `#expect`(
             AgentLaunchEnvironmentPolicy.selectedEnvironment(
                 from: [
                     "CUSTOM_BASE_URL": "http://subrouter-team:31415/v1",
                     "HERMES_CODEX_BASE_URL": "http://subrouter-team:31415/backend-api/codex",
                 ],
                 kind: "hermes-agent"
             ) == [
                 "CUSTOM_BASE_URL": "http://subrouter-team:31415/v1",
                 "HERMES_CODEX_BASE_URL": "http://subrouter-team:31415/backend-api/codex",
             ]
         )
+    }
+
+    `@Test`("Filters Hermes Codex URLs for non-Hermes agent kinds")
+    func filtersHermesCodexURLsForNonHermesAgentKinds() {
         `#expect`(
             AgentLaunchEnvironmentPolicy.selectedEnvironment(
                 from: [
                     "CUSTOM_BASE_URL": "http://subrouter-team:31415/v1",
                     "HERMES_CODEX_BASE_URL": "http://subrouter-team:31415/backend-api/codex",
                 ],
                 kind: "codex"
             ).isEmpty
         )
     }
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/HermesAgentCodexEnvironmentTests.swift`
around lines 118 - 126, The assertion that
AgentLaunchEnvironmentPolicy.selectedEnvironment(..., kind: "codex").isEmpty
should be split into its own unit test: keep the existing test that verifies
allowed Hermes Codex URLs focused on the positive case (allowing URLs) and move
the blocking assertion into a new test named e.g.
testBlockingHermesCodexEnvironment_whenCustomBaseUrlConflicts; locate the call
to AgentLaunchEnvironmentPolicy.selectedEnvironment and the `#expect`(...)
assertion in HermesAgentCodexEnvironmentTests.swift, create a separate test
function that only checks the .isEmpty result for kind: "codex", and ensure each
test has a clear descriptive name and asserts a single scenario.

This branch was successfully deployed

1 active deployment
Preview – cmux — e8e16136 Deployed May 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant