Repository navigation
Add Codex CLI hooks integration - #2103
Conversation
Adds `cmux codex install-hooks` to install lifecycle hooks into
~/.codex/hooks.json and enable the codex_hooks feature flag. The hooks
call `cmux codex-hook <event>` which gracefully no-ops (exit 0, prints
{}) when not running inside cmux, so they're safe to leave installed
globally.
Supported events: SessionStart (session tracking), UserPromptSubmit
(set Running status), Stop (completion notification + Idle status).
Install merges with existing user hooks and is idempotent. Uninstall
(`cmux codex uninstall-hooks`) removes only cmux-owned hooks,
identified by the `cmux codex-hook` command prefix.
install-hooks and uninstall-hooks now preview changes to hooks.json and
config.toml before applying, with a [Y/n] prompt. Pass --yes/-y to
skip confirmation.
Hook commands use `command -v cmux` guard so they silently no-op
(echo '{}') when cmux CLI is not on PATH (e.g. user runs codex in a
non-cmux terminal or after uninstalling cmux).
install-hooks and uninstall-hooks now show unified-diff-style output with line numbers and surrounding context lines, making it easier to see exactly what will change in hooks.json and config.toml.
The hook shell command now checks [ -n "$CMUX_SURFACE_ID" ] first,
so it short-circuits to echo '{}' without ever invoking cmux when
not inside a cmux terminal. Prevents usage text and socket errors
from leaking into Codex hook output.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis change adds new CLI commands to manage Codex integration and hook events: Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR adds Codex CLI hooks integration to cmux: Key issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant cmux CLI
participant hooks.json
participant config.toml
participant Codex
User->>cmux CLI: cmux codex install-hooks
cmux CLI->>hooks.json: read existing (if any)
cmux CLI->>config.toml: read existing (if any)
cmux CLI-->>User: show diff [Y/n]
User->>cmux CLI: confirm
cmux CLI->>hooks.json: write merged hooks
cmux CLI->>config.toml: write codex_hooks = true
Note over User,Codex: Inside cmux session
Codex->>cmux CLI: SessionStart → cmux codex-hook session-start
cmux CLI->>cmux CLI: resolve workspaceId + surfaceId
cmux CLI->>cmux CLI: sessionStore.upsert(sessionId)
Codex->>cmux CLI: UserPromptSubmit → cmux codex-hook prompt-submit
cmux CLI->>cmux CLI: clear_notifications
cmux CLI->>cmux CLI: set_status codex Running
Codex->>cmux CLI: Stop → cmux codex-hook stop
cmux CLI->>cmux CLI: notify_target (completion)
cmux CLI->>cmux CLI: set_status codex Idle
User->>cmux CLI: cmux codex uninstall-hooks
cmux CLI->>hooks.json: remove cmux hooks
Note over config.toml: ⚠ config.toml NOT reverted
Reviews (1): Last reviewed commit: "Check CMUX_SURFACE_ID in shell guard bef..." | Re-trigger Greptile |
| private func runCodexUninstallHooks() throws { | ||
| let skipConfirm = ProcessInfo.processInfo.arguments.contains("--yes") | ||
| || ProcessInfo.processInfo.arguments.contains("-y") | ||
| let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"] | ||
| ?? NSString(string: "~/.codex").expandingTildeInPath | ||
| let hooksPath = (codexHome as NSString).appendingPathComponent("hooks.json") | ||
| let fm = FileManager.default | ||
|
|
||
| guard fm.fileExists(atPath: hooksPath), | ||
| let data = try? Data(contentsOf: URL(fileURLWithPath: hooksPath)), | ||
| var parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { | ||
| print("No hooks.json found at \(hooksPath)") | ||
| return | ||
| } | ||
|
|
||
| guard var hooks = parsed["hooks"] as? [String: Any] else { | ||
| print("No hooks section found in \(hooksPath)") | ||
| return | ||
| } | ||
|
|
||
| // Build the new state without cmux hooks | ||
| var removedCount = 0 | ||
| for eventName in hooks.keys { | ||
| guard var eventGroups = hooks[eventName] as? [[String: Any]] else { continue } | ||
| let before = eventGroups.count | ||
| eventGroups.removeAll { group in | ||
| guard let groupHooks = group["hooks"] as? [[String: Any]] else { return false } | ||
| return groupHooks.allSatisfy { hook in | ||
| (hook["command"] as? String)?.contains(Self.codexHookCommandMarker) == true | ||
| } | ||
| } | ||
| removedCount += before - eventGroups.count | ||
| if eventGroups.isEmpty { | ||
| hooks.removeValue(forKey: eventName) | ||
| } else { | ||
| hooks[eventName] = eventGroups | ||
| } | ||
| } | ||
|
|
||
| if removedCount == 0 { | ||
| print("No cmux hooks found in \(hooksPath)") | ||
| return | ||
| } | ||
|
|
||
| parsed["hooks"] = hooks | ||
| let newJsonData = try JSONSerialization.data(withJSONObject: parsed, options: [.prettyPrinted, .sortedKeys]) | ||
| let newContent = String(data: newJsonData, encoding: .utf8) ?? "" | ||
| let oldContent = String(data: data, encoding: .utf8) ?? "" | ||
|
|
||
| // Show diff and ask for confirmation | ||
| print(" \(hooksPath):") | ||
| printSimpleDiff(old: oldContent, new: newContent) | ||
| print("") | ||
|
|
||
| if !skipConfirm { | ||
| print("Remove \(removedCount) cmux hook(s)? [Y/n] ", terminator: "") | ||
| if let response = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), | ||
| !response.isEmpty && response != "y" && response != "yes" { | ||
| print("Aborted.") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| try newJsonData.write(to: URL(fileURLWithPath: hooksPath), options: .atomic) | ||
| print("Removed \(removedCount) cmux hook(s).") |
There was a problem hiding this comment.
Uninstall doesn't revert
config.toml
runCodexInstallHooks modifies two files: hooks.json and config.toml (adding codex_hooks = true under [features]). But runCodexUninstallHooks only removes entries from hooks.json — the configPath variable isn't even read here. After an uninstall, config.toml retains the codex_hooks = true entry, leaving the user's config in a partially-reverted state.
The fix would be to also compute the configPath, strip the codex_hooks key (and potentially the [features] section if it was newly created by the install), include the config.toml diff in the confirmation prompt, and write it back after confirmation — symmetric with what runCodexInstallHooks does.
| } else if result.contains("[features]") { | ||
| result = result.replacingOccurrences( | ||
| of: "[features]", | ||
| with: "[features]\ncodex_hooks = true" | ||
| ) |
There was a problem hiding this comment.
replacingOccurrences replaces every [features] in the file
String.replacingOccurrences(of:with:) replaces all matches, not just the first one. If a user's config.toml has [features] appearing in a comment (e.g. # see [features] for options), this will inject codex_hooks = true after every such occurrence, producing invalid TOML and corrupting the user's config.
Use a line-by-line scan to replace only the first bare [features] section header (a line whose trimmed content equals exactly [features]), similar to how the codex_hooks replacement is already done:
} else if result.contains("[features]") {
let lines = result.components(separatedBy: "\n")
var output: [String] = []
var inserted = false
for line in lines {
output.append(line)
if !inserted && line.trimmingCharacters(in: .whitespaces) == "[features]" {
output.append("codex_hooks = true")
inserted = true
}
}
result = output.joined(separator: "\n")
}| } | ||
|
|
||
| var hooks = existing["hooks"] as? [String: Any] ?? [:] | ||
| let cmuxHooks = Self.codexHooksJSON["hooks"] as! [String: Any] |
There was a problem hiding this comment.
Force cast on static dictionary
Self.codexHooksJSON["hooks"] as! [String: Any] will trap at runtime if codexHooksJSON is ever modified so that the "hooks" key is missing or holds a different type. While the property is private static and currently stable, the force cast is unnecessary — a guard let keeps the same invariant without the crash risk:
| let cmuxHooks = Self.codexHooksJSON["hooks"] as! [String: Any] | |
| guard let cmuxHooks = Self.codexHooksJSON["hooks"] as? [String: Any] else { return } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4209ca6ce2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // so help text is available even when cmux is not running. | ||
| if command != "__tmux-compat", | ||
| command != "claude-teams", | ||
| command != "codex", |
There was a problem hiding this comment.
Route codex help flags through subcommand help
Excluding codex from the pre-socket help path means cmux codex --help (and cmux codex install-hooks --help) never reach dispatchSubcommandHelp; instead the command falls through to normal execution, which can attempt a socket connection or even start the install flow. This breaks expected CLI semantics for --help and can trigger side effects when the user only requested usage text.
Useful? React with 👍 / 👎.
| if result.contains("codex_hooks") { | ||
| let lines = result.components(separatedBy: "\n") | ||
| var output: [String] = [] | ||
| for line in lines { | ||
| let trimmed = line.trimmingCharacters(in: .whitespaces) | ||
| if trimmed.hasPrefix("codex_hooks") && trimmed.contains("=") { | ||
| output.append("codex_hooks = true") |
There was a problem hiding this comment.
Parse codex_hooks as a key, not any substring
Using result.contains("codex_hooks") as the gate is too broad: if config.toml only mentions that token in a comment or in another key name, this branch runs but no actual codex_hooks = ... line is added, so installation can leave hooks disabled. The same logic can also rewrite similarly-prefixed keys because hasPrefix("codex_hooks") matches more than the exact setting name.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 11195-11260: runCodexUninstallHooks only rewrites hooks.json but
doesn't revert the [features].codex_hooks flag set during install; update
runCodexUninstallHooks to also load the Codex config (codexHome/config.toml),
parse it (preserving formatting/comments if possible), check whether
features.codex_hooks is present/true, toggle it to false if appropriate (i.e.,
when removed cmux hooks count > 0 and no other integrations require it), produce
a simple diff of the old vs new config.toml, prompt the user with the same
skipConfirm logic, and write the updated config.toml atomically only after
confirmation (use the same readLine/skipConfirm flow and error handling used for
hooks.json); refer to runCodexUninstallHooks, Self.codexHookCommandMarker,
hooksPath and add handling for a new configPath variable for config.toml and use
the same diff/confirm/write pattern.
- Around line 11097-11106: The current read/parse logic silently treats an
existing but unreadable/invalid hooks.json as empty (see existingHooksContent,
data, parsed and resulting existing = [:]), which can overwrite user data;
change this to hard-fail: if fileExists(atPath: hooksPath) but reading or
JSONSerialization.jsonObject(with:) fails, return/throw/log an error and abort
the install rather than defaulting to an empty dictionary; apply the identical
pattern to the config.toml read logic (the config content/path variables used in
that block) and to the uninstall code path that currently treats unreadable
hooks.json as "not found" so it similarly reports an error instead of
proceeding.
- Around line 11081-11082: The uninstall logic currently uses string containment
with codexHookCommandMarker which can falsely match user hooks and fails when a
group is mixed; change it to match exact generated commands by creating a Set of
exact codex-owned command strings (use the same codexHookCommand(...) helper to
build them) and replace the current contains/allSatisfy logic with a function
(e.g., removingOwnedCodexHooks) that iterates groups, filters each group's
"hooks" array removing only hooks whose "command" exactly equals one of the
codexOwnedCommands, updates the group's "hooks" entry, and returns nil to drop
the group only when its hooks array becomes empty (leave mixed groups intact).
- Around line 1467-1477: The codex branch currently only handles
install-hooks/uninstall-hooks and then falls through causing the socket to open
for bare/unknown/help invocations and misroutes trailing args; update dispatch
to validate the codex subcommand early by extracting let subcommand =
commandArgs.first?.lowercased() ?? "help" and subArgs =
Array(commandArgs.dropFirst()), then switch on subcommand to: call
runCodexInstallHooks(commandArgs: subArgs) for "install-hooks", call
runCodexUninstallHooks(commandArgs: subArgs) for "uninstall-hooks", print usage
for "help"/"--help"/"-h", and throw a CLIError for unknown subcommands so the
branch returns without opening the socket and helpers receive the correct
subArgs instead of reading ProcessInfo.
| // Codex hooks management (no socket needed) | ||
| if command == "codex" { | ||
| let sub = commandArgs.first?.lowercased() ?? "help" | ||
| if sub == "install-hooks" { | ||
| try runCodexInstallHooks() | ||
| return | ||
| } else if sub == "uninstall-hooks" { | ||
| try runCodexUninstallHooks() | ||
| return | ||
| } | ||
| } |
There was a problem hiding this comment.
Validate codex subcommands before this branch falls through.
cmux codex, cmux codex --help, and unknown subcommands currently fall past this block, try to open the socket, and then fail as an unknown top-level command. Because the install/uninstall helpers only inspect ProcessInfo.processInfo.arguments for --yes, unexpected trailing args are also ignored while still editing user config. Handle help/unknown subcommands here and pass validated subcommand args into the helpers.
Suggested dispatch shape
- if command == "codex" {
- let sub = commandArgs.first?.lowercased() ?? "help"
- if sub == "install-hooks" {
- try runCodexInstallHooks()
- return
- } else if sub == "uninstall-hooks" {
- try runCodexUninstallHooks()
- return
- }
- }
+ if command == "codex" {
+ try runCodex(commandArgs: commandArgs)
+ return
+ }private func runCodex(commandArgs: [String]) throws {
let subcommand = commandArgs.first?.lowercased() ?? "help"
let subArgs = Array(commandArgs.dropFirst())
switch subcommand {
case "install-hooks":
try runCodexInstallHooks(commandArgs: subArgs)
case "uninstall-hooks":
try runCodexUninstallHooks(commandArgs: subArgs)
case "help", "--help", "-h":
print(subcommandUsage("codex") ?? "")
default:
throw CLIError(message: "Unknown codex subcommand: \(subcommand)")
}
}Also applies to: 11085-11086, 11196-11197
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLI/cmux.swift` around lines 1467 - 1477, The codex branch currently only
handles install-hooks/uninstall-hooks and then falls through causing the socket
to open for bare/unknown/help invocations and misroutes trailing args; update
dispatch to validate the codex subcommand early by extracting let subcommand =
commandArgs.first?.lowercased() ?? "help" and subArgs =
Array(commandArgs.dropFirst()), then switch on subcommand to: call
runCodexInstallHooks(commandArgs: subArgs) for "install-hooks", call
runCodexUninstallHooks(commandArgs: subArgs) for "uninstall-hooks", print usage
for "help"/"--help"/"-h", and throw a CLIError for unknown subcommands so the
branch returns without opening the socket and helpers receive the correct
subArgs instead of reading ProcessInfo.
| /// Identifier used to detect cmux-owned hooks during uninstall. | ||
| private static let codexHookCommandMarker = "cmux codex-hook" |
There was a problem hiding this comment.
Owned-hook matching needs to be exact and per hook entry.
The current contains("cmux codex-hook") + allSatisfy logic has two bad cases: a user hook that happens to invoke cmux codex-hook can be treated as cmux-owned, and a mixed group will keep the cmux hook forever. Match the exact commands this installer generates and remove owned hooks individually, then drop the group only when it becomes empty.
Suggested owned-hook filtering
private static let codexOwnedCommands: Set<String> = [
codexHookCommand("session-start"),
codexHookCommand("prompt-submit"),
codexHookCommand("stop"),
]
private func removingOwnedCodexHooks(from groups: [[String: Any]]) -> [[String: Any]] {
groups.compactMap { group in
guard var hooks = group["hooks"] as? [[String: Any]] else { return group }
hooks.removeAll { hook in
guard let command = hook["command"] as? String else { return false }
return Self.codexOwnedCommands.contains(command)
}
guard !hooks.isEmpty else { return nil }
var updated = group
updated["hooks"] = hooks
return updated
}
}Also applies to: 11114-11120, 11220-11224
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLI/cmux.swift` around lines 11081 - 11082, The uninstall logic currently
uses string containment with codexHookCommandMarker which can falsely match user
hooks and fails when a group is mixed; change it to match exact generated
commands by creating a Set of exact codex-owned command strings (use the same
codexHookCommand(...) helper to build them) and replace the current
contains/allSatisfy logic with a function (e.g., removingOwnedCodexHooks) that
iterates groups, filters each group's "hooks" array removing only hooks whose
"command" exactly equals one of the codexOwnedCommands, updates the group's
"hooks" entry, and returns nil to drop the group only when its hooks array
becomes empty (leave mixed groups intact).
| let existingHooksContent: String? = fm.fileExists(atPath: hooksPath) | ||
| ? (try? String(contentsOfFile: hooksPath, encoding: .utf8)) | ||
| : nil | ||
|
|
||
| // Build merged hooks | ||
| var existing: [String: Any] = [:] | ||
| if let existingHooksContent, | ||
| let data = existingHooksContent.data(using: .utf8), | ||
| let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { | ||
| existing = parsed |
There was a problem hiding this comment.
Do not coerce existing Codex files to empty state on read/parse failure.
If hooks.json or config.toml exists but can't be read or parsed, install silently treats it as empty and writes fresh content. That can overwrite a user's existing Codex setup after a malformed JSON file, encoding issue, or partial write. Uninstall should likewise report invalid hooks.json instead of "not found".
Suggested hard-fail on existing unreadable files
- let existingHooksContent: String? = fm.fileExists(atPath: hooksPath)
- ? (try? String(contentsOfFile: hooksPath, encoding: .utf8))
- : nil
+ let existingHooksContent: String?
+ if fm.fileExists(atPath: hooksPath) {
+ existingHooksContent = try String(contentsOfFile: hooksPath, encoding: .utf8)
+ } else {
+ existingHooksContent = nil
+ }
...
- if let existingHooksContent,
- let data = existingHooksContent.data(using: .utf8),
- let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
- existing = parsed
- }
+ if let existingHooksContent {
+ guard let data = existingHooksContent.data(using: .utf8),
+ let parsed = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ throw CLIError(message: "Invalid Codex hooks JSON at \(hooksPath)")
+ }
+ existing = parsed
+ }Apply the same pattern to config.toml reads and to the uninstall path.
Also applies to: 11128-11130, 11203-11206
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLI/cmux.swift` around lines 11097 - 11106, The current read/parse logic
silently treats an existing but unreadable/invalid hooks.json as empty (see
existingHooksContent, data, parsed and resulting existing = [:]), which can
overwrite user data; change this to hard-fail: if fileExists(atPath: hooksPath)
but reading or JSONSerialization.jsonObject(with:) fails, return/throw/log an
error and abort the install rather than defaulting to an empty dictionary; apply
the identical pattern to the config.toml read logic (the config content/path
variables used in that block) and to the uninstall code path that currently
treats unreadable hooks.json as "not found" so it similarly reports an error
instead of proceeding.
There was a problem hiding this comment.
5 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="CLI/cmux.swift">
<violation number="1" location="CLI/cmux.swift:1469">
P2: `codex` default/help paths fall through to socket dispatch, so `cmux codex` and `cmux codex --help` can fail instead of showing codex usage.</violation>
<violation number="2" location="CLI/cmux.swift:11117">
P2: Using substring ownership detection can delete user-defined hooks that mention `cmux codex-hook`, instead of preserving non-cmux hooks.</violation>
<violation number="3" location="CLI/cmux.swift:11356">
P2: Parse `codex_hooks` as an exact TOML key instead of using a substring gate. The current `result.contains("codex_hooks")` path can trigger on comments or unrelated keys and then skip adding `codex_hooks = true`, leaving hooks disabled.</violation>
<violation number="4" location="CLI/cmux.swift:11361">
P2: The config rewrite matches `codex_hooks*` prefixes, so similarly named TOML keys can be overwritten incorrectly.</violation>
<violation number="5" location="CLI/cmux.swift:11369">
P2: Avoid global `replacingOccurrences` for `[features]`; it can modify every occurrence (including comments) and insert duplicate `codex_hooks` entries. Insert once after the first real `[features]` section header line instead.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| let sub = commandArgs.first?.lowercased() ?? "help" | ||
| if sub == "install-hooks" { | ||
| try runCodexInstallHooks() | ||
| return | ||
| } else if sub == "uninstall-hooks" { | ||
| try runCodexUninstallHooks() | ||
| return | ||
| } |
There was a problem hiding this comment.
P2: codex default/help paths fall through to socket dispatch, so cmux codex and cmux codex --help can fail instead of showing codex usage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 1469:
<comment>`codex` default/help paths fall through to socket dispatch, so `cmux codex` and `cmux codex --help` can fail instead of showing codex usage.</comment>
<file context>
@@ -1463,6 +1464,27 @@ struct CMUXCLI {
+ // Codex hooks management (no socket needed)
+ if command == "codex" {
+ let sub = commandArgs.first?.lowercased() ?? "help"
+ if sub == "install-hooks" {
+ try runCodexInstallHooks()
</file context>
| let sub = commandArgs.first?.lowercased() ?? "help" | |
| if sub == "install-hooks" { | |
| try runCodexInstallHooks() | |
| return | |
| } else if sub == "uninstall-hooks" { | |
| try runCodexUninstallHooks() | |
| return | |
| } | |
| let sub = commandArgs.first?.lowercased() ?? "help" | |
| switch sub { | |
| case "install-hooks": | |
| try runCodexInstallHooks() | |
| return | |
| case "uninstall-hooks": | |
| try runCodexUninstallHooks() | |
| return | |
| case "help", "--help", "-h": | |
| if let text = subcommandUsage("codex") { | |
| print("cmux codex") | |
| print("") | |
| print(text) | |
| } | |
| return | |
| default: | |
| throw CLIError(message: "Unknown codex subcommand: \(sub)") | |
| } |
uninstall-hooks now also removes codex_hooks from config.toml and shows the diff for both files before asking for confirmation. buildConfigWithCodexHooks uses exact TOML key matching instead of substring contains, and inserts after the first [features] header only (not replacingOccurrences which hit all matches).
There was a problem hiding this comment.
2 issues found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="CLI/cmux.swift">
<violation number="1" location="CLI/cmux.swift:11378">
P2: Scope the `codex_hooks` update to the `[features]` section; the current global first-match return can update the wrong table and skip enabling `[features].codex_hooks`.</violation>
<violation number="2" location="CLI/cmux.swift:11403">
P1: Restrict `codex_hooks` deletion to the `[features]` section; current global removal can erase unrelated user config.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| var lines = content.components(separatedBy: "\n") | ||
|
|
||
| // Remove the codex_hooks line | ||
| lines.removeAll { isTomlKey($0, key: "codex_hooks") } |
There was a problem hiding this comment.
P1: Restrict codex_hooks deletion to the [features] section; current global removal can erase unrelated user config.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 11403:
<comment>Restrict `codex_hooks` deletion to the `[features]` section; current global removal can erase unrelated user config.</comment>
<file context>
@@ -11351,33 +11372,58 @@ struct CMUXCLI {
+ var lines = content.components(separatedBy: "\n")
+
+ // Remove the codex_hooks line
+ lines.removeAll { isTomlKey($0, key: "codex_hooks") }
+
+ // If [features] section is now empty (only has the header, nothing before next section or EOF),
</file context>
| lines.removeAll { isTomlKey($0, key: "codex_hooks") } | |
| if let featuresIdx = lines.firstIndex(where: { $0.trimmingCharacters(in: .whitespaces) == "[features]" }) { | |
| let sectionEnd = lines[(featuresIdx + 1)...].firstIndex(where: { | |
| $0.trimmingCharacters(in: .whitespaces).hasPrefix("[") | |
| }) ?? lines.count | |
| for i in stride(from: sectionEnd - 1, through: featuresIdx + 1, by: -1) { | |
| if isTomlKey(lines[i], key: "codex_hooks") { | |
| lines.remove(at: i) | |
| } | |
| } | |
| } |
| if let idx = lines.firstIndex(where: { isTomlKey($0, key: "codex_hooks") }) { | ||
| lines[idx] = "codex_hooks = true" | ||
| return lines.joined(separator: "\n") | ||
| } |
There was a problem hiding this comment.
P2: Scope the codex_hooks update to the [features] section; the current global first-match return can update the wrong table and skip enabling [features].codex_hooks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 11378:
<comment>Scope the `codex_hooks` update to the `[features]` section; the current global first-match return can update the wrong table and skip enabling `[features].codex_hooks`.</comment>
<file context>
@@ -11351,33 +11372,58 @@ struct CMUXCLI {
+ var lines = content.components(separatedBy: "\n")
+
+ // Check if codex_hooks key already exists (exact key match at line start)
+ if let idx = lines.firstIndex(where: { isTomlKey($0, key: "codex_hooks") }) {
+ lines[idx] = "codex_hooks = true"
+ return lines.joined(separator: "\n")
</file context>
| if let idx = lines.firstIndex(where: { isTomlKey($0, key: "codex_hooks") }) { | |
| lines[idx] = "codex_hooks = true" | |
| return lines.joined(separator: "\n") | |
| } | |
| if let featuresIdx = lines.firstIndex(where: { $0.trimmingCharacters(in: .whitespaces) == "[features]" }) { | |
| let sectionEnd = lines[(featuresIdx + 1)...].firstIndex(where: { | |
| $0.trimmingCharacters(in: .whitespaces).hasPrefix("[") | |
| }) ?? lines.count | |
| if let existingIdx = (featuresIdx + 1..<sectionEnd).first(where: { isTomlKey(lines[$0], key: "codex_hooks") }) { | |
| lines[existingIdx] = "codex_hooks = true" | |
| return lines.joined(separator: "\n") | |
| } | |
| } |
Ingests all upstream fixes since 2026-03-22 including: - Fix Cmd+N crash: retain snapshot workspaces (manaflow-ai#2183, manaflow-ai#2181, manaflow-ai#2178, manaflow-ai#2173) - Fix browser pane restore after reopen (manaflow-ai#2141) - Fix Ghostty resize_split keybind (manaflow-ai#1899) - Reduce shell integration prompt latency (manaflow-ai#2109) - Fix command palette focus after terminal find (manaflow-ai#2089) - Add Codex CLI hooks (manaflow-ai#2103) - Add cmux.json custom commands (manaflow-ai#2011) - Fix window position restore on relaunch (manaflow-ai#2129) Conflict resolution: - BrowserPanel.swift: accepted upstream configureWebViewConfiguration() refactor (already includes our forMainFrameOnly:true CAPTCHA fix from PR manaflow-ai#1877) Fork-specific files preserved: - Sources/Panels/WebAuthn{Coordinator,BridgeJavaScript}.swift - Sources/FIDO2/module.modulemap - vendor/ctap2 submodule - cmux.entitlements (with camera/audio-input removed) - cmux.embedded.entitlements - .github/workflows/fork-{ci,release}.yml
* Add Codex CLI hooks integration
Adds `cmux codex install-hooks` to install lifecycle hooks into
~/.codex/hooks.json and enable the codex_hooks feature flag. The hooks
call `cmux codex-hook <event>` which gracefully no-ops (exit 0, prints
{}) when not running inside cmux, so they're safe to leave installed
globally.
Supported events: SessionStart (session tracking), UserPromptSubmit
(set Running status), Stop (completion notification + Idle status).
Install merges with existing user hooks and is idempotent. Uninstall
(`cmux codex uninstall-hooks`) removes only cmux-owned hooks,
identified by the `cmux codex-hook` command prefix.
* Show diff and ask for confirmation before modifying user config
install-hooks and uninstall-hooks now preview changes to hooks.json and
config.toml before applying, with a [Y/n] prompt. Pass --yes/-y to
skip confirmation.
Hook commands use `command -v cmux` guard so they silently no-op
(echo '{}') when cmux CLI is not on PATH (e.g. user runs codex in a
non-cmux terminal or after uninstalling cmux).
* Improve diff output with line numbers and context
install-hooks and uninstall-hooks now show unified-diff-style output
with line numbers and surrounding context lines, making it easier to
see exactly what will change in hooks.json and config.toml.
* Check CMUX_SURFACE_ID in shell guard before calling cmux
The hook shell command now checks [ -n "$CMUX_SURFACE_ID" ] first,
so it short-circuits to echo '{}' without ever invoking cmux when
not inside a cmux terminal. Prevents usage text and socket errors
from leaking into Codex hook output.
* Uninstall reverts config.toml; fix [features] section handling
uninstall-hooks now also removes codex_hooks from config.toml and
shows the diff for both files before asking for confirmation.
buildConfigWithCodexHooks uses exact TOML key matching instead of
substring contains, and inserts after the first [features] header
only (not replacingOccurrences which hit all matches).
---------
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Summary
cmux codex install-hooks/uninstall-hooksto manage lifecycle hooks in~/.codex/hooks.jsoncmux codex-hookhandlers (session-start, prompt-submit, stop) for sidebar status and notificationsCMUX_SURFACE_IDbefore invokingcmux, falls back toecho '{}')Test plan
cmux codex install-hooks, verify diff shown with line numbers and[Y/n]prompt~/.codex/hooks.jsonandconfig.tomlupdated correctlycmux codex install-hooksagain, verify "already installed" messagecodexinside cmux, verify sidebar shows Running/Idle statuscodexoutside cmux (e.g. default Terminal), verify no hook errors or junk outputcmux codex uninstall-hooks, verify only cmux hooks removed, user hooks preservedSummary by cubic
Adds Codex CLI hooks integration to
cmuxto track sessions, show sidebar status, and send completion notifications. Install/uninstall safely manage~/.codex/hooks.jsonandconfig.toml(including revertingcodex_hookson uninstall) and no-op outsidecmux.New Features
cmux codex install-hooks/uninstall-hooksmanage hooks and togglecodex_hooksinconfig.toml; uninstall removes the flag and cleans an empty[features]section with exact TOML key matching.cmux codex-hookhandlers:session-start,prompt-submit(sets Running),stop(notification + sets Idle).cmux: checksCMUX_SURFACE_IDandcommand -v cmux; prints{}and exits 0.cmux codex-hookentries; idempotent.[Y/n]confirm; use--yes/-yto skip.Migration
cmux codex install-hooksto enable Codex hooks.Written for commit 811e806. Summary will update on new commits.
Summary by CodeRabbit
New Features
cmux codex install-hooksandcmux codex uninstall-hooksto manage Codex integration without requiring a cmux socketcmux codex-hookcommand to handle Codex hook events during session and prompt workflows