Replace bootstrap file handoff with CLI flags for local runs
`quire-ci run` now accepts `--git-dir`, `--sha`, and `--git-ref`
directly. The server passes them as CLI args when transport is None
(local `quire ci run`). No files are written or read at any point in
either path. `write_bootstrap`, `load_bootstrap`, and `--bootstrap`
are removed entirely.

https://claude.ai/code/session_01Ngf4zbFf87zJFUR5ee2Y8b
change
commit 01ff292b784633f3d6f6be7ba03bd8834b150d38
author Claude <noreply@anthropic.com>
date
parent be501cf3
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 7a42e0e..e19de27 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -7,7 +7,7 @@ use std::rc::Rc;
 
 use facet::Facet;
 use figue::{self as args, Driver, FigueBuiltins};
-use miette::{IntoDiagnostic, Result, bail};
+use miette::{IntoDiagnostic, Result};
 use quire_core::api::SecretResponse;
 use quire_core::ci::bootstrap::Bootstrap;
 use quire_core::ci::event::{Event, EventKind, JobOutcome, RunOutcome};
@@ -106,11 +106,20 @@ enum Commands {
         #[facet(args::named, default)]
         out_dir: Option<PathBuf>,
 
-        /// Path to a JSON bootstrap file produced by the orchestrator.
-        /// Required for local runs (no `QUIRE__SERVER_URL`); omitted for
-        /// server-dispatched runs, which fetch bootstrap via the API.
+        /// Path to the bare git repo for this run. Required for local
+        /// runs (no `QUIRE__SERVER_URL`); server-dispatched runs
+        /// receive this via the bootstrap API instead.
         #[facet(args::named, default)]
-        bootstrap: Option<PathBuf>,
+        git_dir: Option<PathBuf>,
+
+        /// Commit SHA for this run. Required for local runs.
+        #[facet(args::named, default)]
+        sha: Option<String>,
+
+        /// Git ref for this run (e.g. `refs/heads/main`). Required for
+        /// local runs.
+        #[facet(args::named, default)]
+        git_ref: Option<String>,
     },
 }
 
@@ -275,7 +284,9 @@ fn main() -> Result<()> {
         Commands::Run {
             events,
             out_dir,
-            bootstrap,
+            git_dir,
+            sha,
+            git_ref,
         } => {
             let sink: Box<dyn EventSink> = match events.parse::<EventsTarget>().unwrap() {
                 EventsTarget::Null => Box::new(NullSink),
@@ -315,10 +326,17 @@ fn main() -> Result<()> {
             let client = RunClient::new(session.clone());
 
             let (git_dir, meta, sentry_trace_id) = if session.server_url.is_empty() {
-                let Some(path) = bootstrap else {
-                    bail!("--bootstrap is required for local runs (no QUIRE__SERVER_URL set)");
+                let git_dir = git_dir
+                    .ok_or_else(|| miette::miette!("--git-dir is required for local runs"))?;
+                let sha = sha.ok_or_else(|| miette::miette!("--sha is required for local runs"))?;
+                let git_ref = git_ref
+                    .ok_or_else(|| miette::miette!("--git-ref is required for local runs"))?;
+                let meta = RunMeta {
+                    sha,
+                    r#ref: git_ref,
+                    pushed_at: jiff::Timestamp::now(),
                 };
-                load_bootstrap(&path)?
+                (git_dir, meta, None)
             } else {
                 client.fetch_bootstrap()?
             };
@@ -407,27 +425,6 @@ fn validate(workspace: PathBuf) -> Result<()> {
     Ok(())
 }
 
-/// Read and parse the bootstrap file the orchestrator wrote before
-/// spawning.
-///
-/// Unlinks the file as soon as the bytes are in memory — getting it off
-/// disk early limits the blast radius of a later panic or crash leaving
-/// a 0600 file behind.
-fn load_bootstrap(path: &std::path::Path) -> Result<(PathBuf, RunMeta, Option<String>)> {
-    let bytes = fs_err::read(path).into_diagnostic()?;
-    if let Err(e) = fs_err::remove_file(path) {
-        // Don't abort — the bytes are already loaded and the server
-        // will best-effort unlink after we exit. But this is a
-        // security-relevant cleanup, so it's worth surfacing.
-        eprintln!(
-            "warning: failed to remove bootstrap file {}: {e}",
-            path.display()
-        );
-    }
-    let bootstrap: Bootstrap = serde_json::from_slice(&bytes).into_diagnostic()?;
-    Ok((bootstrap.git_dir, bootstrap.meta, bootstrap.sentry_trace_id))
-}
-
 fn run_pipeline(
     workspace: PathBuf,
     mut sink: Box<dyn EventSink>,
@@ -616,29 +613,4 @@ mod tests {
         };
         assert_eq!(path, PathBuf::from("/tmp/run.jsonl"));
     }
-
-    #[test]
-    fn load_bootstrap_unlinks_after_read() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let path = dir.path().join("bootstrap.json");
-        let bootstrap = Bootstrap {
-            meta: RunMeta {
-                sha: "0".repeat(40),
-                r#ref: "HEAD".to_string(),
-                pushed_at: jiff::Timestamp::now(),
-            },
-            git_dir: PathBuf::from("/tmp/repo.git"),
-            sentry_trace_id: None,
-        };
-        fs_err::write(&path, serde_json::to_vec(&bootstrap).unwrap()).expect("write");
-
-        let (git_dir, meta, sentry_trace_id) = load_bootstrap(&path).expect("load");
-        assert!(
-            !path.exists(),
-            "bootstrap file should be removed after read"
-        );
-        assert_eq!(git_dir, PathBuf::from("/tmp/repo.git"));
-        assert_eq!(meta.r#ref, "HEAD");
-        assert!(sentry_trace_id.is_none());
-    }
 }
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index 8d23325..ed6b049 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -1,11 +1,9 @@
-//! Wire format for handing off a run from the orchestrator to
-//! `quire-ci`.
+//! Wire format for the bootstrap API response.
 //!
-//! For server-dispatched runs the orchestrator stores a [`Bootstrap`]
-//! in the database; `quire-ci` fetches it via `GET /api/run/bootstrap`
-//! using the per-run bearer token. For local dev runs without a server
-//! the orchestrator writes the JSON to a file and passes the path via
-//! `--bootstrap`.
+//! The orchestrator stores a [`Bootstrap`] in the database; `quire-ci`
+//! fetches it via `GET /api/run/bootstrap` using the per-run bearer
+//! token. Local runs receive bootstrap data directly via CLI flags
+//! (`--git-dir`, `--sha`, `--git-ref`) instead.
 
 use std::path::PathBuf;
 
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index f80b58f..d6dfe69 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -318,7 +318,6 @@ impl Run {
         let run_dir = self.path();
         let log_path = run_dir.join("quire-ci.log");
         let events_path = run_dir.join("events.jsonl");
-        let bootstrap_path = run_dir.join("bootstrap.json");
         // fs_err for the path-bearing IO error; unwrap to std::fs::File so
         // it's convertible into Stdio.
         let log = fs_err::File::create(&log_path)?.into_parts().0;
@@ -342,8 +341,12 @@ impl Run {
 
         match transport {
             None => {
-                write_bootstrap(&bootstrap_path, git_dir, meta, sentry_trace_id)?;
-                cmd.arg("--bootstrap").arg(&bootstrap_path);
+                cmd.arg("--git-dir")
+                    .arg(git_dir)
+                    .arg("--sha")
+                    .arg(&meta.sha)
+                    .arg("--git-ref")
+                    .arg(&meta.r#ref);
             }
             Some(t) => {
                 self.store_bootstrap_data(git_dir, sentry_trace_id)?;
@@ -365,21 +368,6 @@ impl Run {
                 source,
             })?;
 
-        // For local runs (None transport), quire-ci unlinks the bootstrap
-        // file after reading it. This is a best-effort safety net for
-        // paths where it didn't get that far. `NotFound` is expected.
-        if transport.is_none()
-            && let Err(e) = fs_err::remove_file(&bootstrap_path)
-            && e.kind() != std::io::ErrorKind::NotFound
-        {
-            tracing::warn!(
-                run_id = %self.id,
-                path = %bootstrap_path.display(),
-                error = %e,
-                "failed to remove bootstrap file after run"
-            );
-        }
-
         // Ingest events before checking outcome — partial results from a
         // crashed run are still useful in the UI. A parse failure is
         // logged but doesn't mask the run outcome.
@@ -641,37 +629,6 @@ impl Run {
     }
 }
 
-/// Serialize the bootstrap payload as JSON and write it to `path` with
-/// owner-only permissions on Unix.
-fn write_bootstrap(
-    path: &Path,
-    git_dir: &Path,
-    meta: &RunMeta,
-    sentry_trace_id: Option<&str>,
-) -> Result<()> {
-    use quire_core::ci::bootstrap::Bootstrap;
-
-    let bootstrap = Bootstrap {
-        meta: meta.clone(),
-        git_dir: git_dir.to_path_buf(),
-        sentry_trace_id: sentry_trace_id.map(Into::into),
-    };
-    let json = serde_json::to_vec_pretty(&bootstrap).map_err(std::io::Error::other)?;
-
-    // Open with mode 0600 from the start so there's no window where
-    // the file is world-readable.
-    use fs_err::os::unix::fs::OpenOptionsExt;
-    use std::io::Write;
-    let mut file = fs_err::OpenOptions::new()
-        .write(true)
-        .create(true)
-        .truncate(true)
-        .mode(0o600)
-        .open(path)?;
-    file.write_all(&json)?;
-    Ok(())
-}
-
 /// Take the final path component of a runs base (`runs/<repo>/`) and
 /// sanitize it for use as the tag segment in `quire-ci/<segment>:<id>`.
 /// Materialize a working tree at `sha` into `workspace` via
@@ -859,24 +816,6 @@ mod tests {
         );
     }
 
-    #[test]
-    fn write_bootstrap_records_git_dir_for_quire_ci() {
-        use quire_core::ci::bootstrap::Bootstrap;
-
-        let dir = tempfile::tempdir().expect("tempdir");
-        let bootstrap_path = dir.path().join("bootstrap.json");
-        let git_dir = dir.path().join("repos").join("test.git");
-
-        write_bootstrap(&bootstrap_path, &git_dir, &test_meta(), None).expect("write_bootstrap");
-
-        let bytes = fs_err::read(&bootstrap_path).expect("read bootstrap");
-        let bootstrap: Bootstrap = serde_json::from_slice(&bytes).expect("parse bootstrap");
-        assert_eq!(
-            bootstrap.git_dir, git_dir,
-            "quire-ci needs the bare repo path to set GIT_DIR for the mirror job"
-        );
-    }
-
     #[test]
     fn run_state_round_trips() {
         for state in [