Hmbown/CodeWhale · error

Inherited interactive terminal takeover is unavailable on…

Error message

Inherited interactive terminal takeover is unavailable on Unix because foreground TTY ownership cannot be transferred safely. Use Bash with `background: true, tty: true`, then continue it with `action: "interact"` and the returned `task_id`; alternatively

What it means

On Unix, the inherited-interactive mode (taking over the caller's foreground terminal for a child) is refused before spawning. POSIX cannot safely transfer foreground TTY ownership here: putting the child in Codewhale's process group would make cooked-mode Ctrl+C terminate both parent and child. The code fails closed until a full POSIX job-control lease exists; persistent PTY tools are the sanctioned interactive lane.

Solutions

  1. Use Bash with `background: true, tty: true`, then continue it with `action: "interact"` and the returned `task_id` (the message's own recommendation).
  2. Use a persistent PTY tool for interactive sessions instead of terminal takeover.
  3. Rewrite the task as non-interactive (flags, stdin data, or config files) and run it synchronously.
  4. On Windows this path may exist; guard interactive calls per-OS or accept the Unix refusal as intended fail-closed behavior.

Example fix

// before
{"command": "python", "interactive": true}          // Unix takeover -> refused
// after
{"command": "python", "background": true, "tty": true} // then action:"interact", task_id
Defensive patterns

Strategy: fallback

Validate before calling

#[cfg(unix)]
if wants_inherited_interactive {
    return Err("use background:true + tty:true + interact on Unix".into());
}

Type guard

fn inherited_interactive_supported() -> bool { cfg!(windows) }

Try / catch

match shell.execute_interactive(cmd) {
    Err(e) if e.to_string().contains("terminal takeover is unavailable") => {
        // fall back to the sanctioned lane
        let id = shell.spawn_background_tty(cmd)?;
        shell.interact(&id, input, false)?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling execute_interactive (inherited interactive terminal path) on Unix without a PTY/persistent-interactive lane — i.e. requesting terminal takeover of the live operator TTY.

Common situations: Trying to run an interactive program (vim, top, a REPL) by hijacking the agent's own terminal on Linux/macOS; porting a Windows interactive flow to Unix.

Understand the failure class

Background: "unsupported platform" / "not supported on this platform" errors: what they mean and how to fix them — this error's family across 47 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/b2a2567c4968d930. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/shell.rs:2135

    pub fn execute_interactive_with_policy_env(
        &mut self,
        command: &str,
        working_dir: Option<&str>,
        timeout_ms: u64,
        policy_override: Option<ExecutionSandboxPolicy>,
        extra_env: HashMap<String, String>,
    ) -> Result<ShellResult> {
        crate::shell_dispatcher::ShellDispatcher::log_exec(command);

        // A new Unix process group that inherits the terminal is not its
        // foreground owner. Letting it read stdin triggers SIGTTIN; sharing
        // Codewhale's group instead would make cooked-mode Ctrl+C terminate
        // both parent and child. Until this path owns a complete POSIX job-
        // control lease, fail closed before spawning. Persistent PTY tools
        // already provide a safe interactive lane without taking over the
        // operator's live terminal.
        if let Some(message) = inherited_interactive_terminal_refusal() {
            return Err(anyhow!(message));
        }

        let work_dir = working_dir.map_or_else(|| self.default_workspace.clone(), PathBuf::from);
        validate_shell_working_dir(&work_dir, working_dir.is_none())?;

        let timeout_ms = timeout_ms.clamp(1000, 600_000);
        let policy = policy_override.unwrap_or_else(|| self.sandbox_policy.clone());

        let spec = CommandSpec::shell(command, work_dir.clone(), Duration::from_millis(timeout_ms))
            .with_policy(policy)
            .with_env(extra_env);
        let exec_env = self.sandbox_manager.prepare(&spec);

        Self::execute_interactive_sandboxed(command, &work_dir, timeout_ms, &exec_env)
    }

    /// Execute command synchronously with timeout (sandboxed).
    fn execute_sync_sandboxed(

View on GitHub (pinned to 433685b202)