openmoat 0.8.0

Policy checks for AI coding agents: decides allow, defer, ask or deny for every tool call from Claude Code, Codex and Cursor, configures Claude Code's and Codex's own OS sandboxes from the same policy, and keeps a local audit log.
//! `moat`: the name OpenMoat's command had up to 0.7.0, kept for a few
//! releases so hooks, scripts and habits that call it keep working.
//!
//! It runs the `openmoat` in its own directory with the same arguments, the
//! same standard streams and the same exit code; on Unix it replaces itself
//! with it. Hooks written by an older `moat init` name this file until
//! `openmoat init` rewrites them, so every failure follows the hook contract:
//! `moat guard` that cannot start `openmoat` exits 2 (deny, ADR-015), any
//! other command 64.

use std::ffi::OsString;
use std::path::PathBuf;
use std::process::{Command, ExitCode};

/// Exit codes of `openmoat`'s contract (`exit.rs`).
const DENY: u8 = 2;
const USAGE: u8 = 64;

fn main() -> ExitCode {
    let args: Vec<OsString> = std::env::args_os().skip(1).collect();
    let failed = if args.first().is_some_and(|arg| arg == "guard") {
        DENY
    } else {
        USAGE
    };
    let Some(openmoat) = beside() else {
        eprintln!(
            "moat: cannot find openmoat next to this moat; reinstall OpenMoat, which installs both"
        );
        return ExitCode::from(failed);
    };
    let mut command = Command::new(&openmoat);
    command.args(&args);
    let error = run(command, failed);
    eprintln!("moat: cannot run {}: {error}", openmoat.display());
    ExitCode::from(failed)
}

/// `openmoat` in this executable's directory, when it is a file.
fn beside() -> Option<PathBuf> {
    let exe = std::env::current_exe().ok()?;
    let name = format!("openmoat{}", std::env::consts::EXE_SUFFIX);
    let path = exe.parent()?.join(name);
    path.is_file().then_some(path)
}

/// Replace this process with `command`; returns only when that failed.
#[cfg(unix)]
fn run(mut command: Command, _failed: u8) -> std::io::Error {
    use std::os::unix::process::CommandExt as _;
    command.exec()
}

/// Run `command` and exit with its code; returns only when it could not start.
#[cfg(not(unix))]
fn run(mut command: Command, failed: u8) -> std::io::Error {
    match command.status() {
        Ok(status) => {
            // A code that does not fit (Windows has 32-bit codes, a crash has
            // none) is a failure, so a crashed `guard` still denies.
            let code = status
                .code()
                .and_then(|c| u8::try_from(c).ok())
                .unwrap_or(failed);
            std::process::exit(i32::from(code))
        }
        Err(error) => error,
    }
}