Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions cmux-tui/crates/cmux-tui/src/agent_hook_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,70 @@ fn hook_command(provider: &str, event: &str) -> String {
)
}

/// Codex's shared app-server may run hooks without the launching terminal's
/// environment. Carry that route in the command, using the embedded helper
/// so hosts deployed with only the cmux-tui binary work too.
#[cfg(unix)]
fn codex_contextual_hook_command(
event: &str,
socket: &Path,
terminal: &str,
launcher: &Path,
) -> String {
format!(
"CMUX_TUI_SOCKET={} CMUX_TUI_TERMINAL_ID={} {} {HOOK_MODE_ARG} codex {} 2>/dev/null||:;echo {{}};#{COMMAND_MARKER}",
shell_quote(&socket.to_string_lossy()),
shell_quote(terminal),
shell_quote(&launcher.to_string_lossy()),
shell_quote(event),
)
}

/// Session flags have their own config layer in Codex 0.162. Trust only the
/// commands generated here, leaving all other hooks' trust decisions intact.
/// Codex appends hooks from lower layers, so disable the installed cmux copies
/// for this invocation instead of sending each event twice.
#[cfg(unix)]
pub(crate) fn codex_session_hook_settings(
socket: &Path,
terminal: &str,
launcher: &Path,
) -> anyhow::Result<Vec<String>> {
let context = Context::runtime()?;
codex_session_hook_settings_with_context(socket, terminal, launcher, &context)
}

#[cfg(unix)]
fn codex_session_hook_settings_with_context(
socket: &Path,
terminal: &str,
launcher: &Path,
context: &Context,
) -> anyhow::Result<Vec<String>> {
let mut settings = Vec::new();
let provider = PROVIDERS.iter().copied().find(|provider| provider.id == "codex").unwrap();
let path = context.provider_path(provider);
let root = read_json_object(&path)?;
let key_path =
codex_state_key_hooks_path(&path, context.provider_path_from_env_override(provider));
for key in codex_owned_entry_keys(&key_path, &root) {
settings.push(format!("hooks.state.{}.enabled=false", toml_edit::Key::new(key)));
}
for event in CODEX_EVENTS {
let command = codex_contextual_hook_command(event, socket, terminal, launcher);
let timeout = codex_hook_timeout(event);
let label = codex_event_state_label(event)?;
let hash = codex_trust_hash(label, &command, timeout);
settings.push(format!(
"hooks.{event}=[{{hooks=[{{type=\"command\",command={},timeout={timeout}}}]}}]",
toml_edit::Value::from(command),
));
let key = toml_edit::Key::new(format!("/<session-flags>/config.toml:{label}:0:0"));
settings.push(format!("hooks.state.{key}.trusted_hash={}", toml_edit::Value::from(hash)));
}
Ok(settings)
}

/// The command shape before the tmux fallback. Its codex trust hashes stay
/// cmux-owned so an upgrade replaces them instead of leaving them behind.
fn legacy_hook_command(provider: &str, event: &str) -> String {
Expand Down
2 changes: 1 addition & 1 deletion cmux-tui/crates/cmux-tui/src/bin/cmux-tui-hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fn run(args: Args, exe_prefix: &[&str]) -> anyhow::Result<()> {
/// own `CMUX_TUI_*` values, or, for an agent in a tmux session started
/// outside cmux-tui (which has none), the cmux-tui terminal attached to the
/// pane's tmux session.
fn session_route() -> Option<(PathBuf, Option<String>)> {
pub(crate) fn session_route() -> Option<(PathBuf, Option<String>)> {
if let Some(socket) = env::var_os("CMUX_TUI_SOCKET").filter(|value| !value.is_empty()) {
let terminal = env::var("CMUX_TUI_TERMINAL_ID").ok().filter(|value| !value.is_empty());
return Some((PathBuf::from(socket), terminal));
Expand Down
Loading
Loading