diff --git a/apps/system/components/DebugConsole/DebugConsole.camkes b/apps/system/components/DebugConsole/DebugConsole.camkes
index 1921837..185c487 100644
--- a/apps/system/components/DebugConsole/DebugConsole.camkes
+++ b/apps/system/components/DebugConsole/DebugConsole.camkes
@@ -27,10 +27,10 @@ component DebugConsole {
   dataport Buf(0x1000000) cpio_archive;
 
   dataport Buf tx_dataport;
-  uses rust_write_inf uart_write;
+  maybe uses rust_write_inf uart_write;
 
   dataport Buf rx_dataport;
-  uses rust_read_inf uart_read;
+  maybe uses rust_read_inf uart_read;
 
   provides LoggerInterface logger;
   uses MemoryInterface memory;
diff --git a/apps/system/components/DebugConsole/kata-debug-console/Cargo.toml b/apps/system/components/DebugConsole/kata-debug-console/Cargo.toml
index 2643acb..49a7191 100644
--- a/apps/system/components/DebugConsole/kata-debug-console/Cargo.toml
+++ b/apps/system/components/DebugConsole/kata-debug-console/Cargo.toml
@@ -20,18 +20,28 @@ edition = "2021"
 description = "Kata OS DebugConsole"
 
 [features]
-default = []
+default = [
+    "autostart_support",
+    "sparrow_uart_support",
+]
+sparrow_uart_support = ["kata-uart-client"]
+autostart_support = ["default-uart-client"]
 # Log level is Info unless LOG_DEBUG or LOG_TRACE are specified
 LOG_DEBUG = []
 LOG_TRACE = []
 
 [dependencies]
-panic-halt = "0.2.0"
+cpio = { git = "https://github.com/rcore-os/cpio" }
+# Disable default so we don't pull in CString which requires an allocator
+cstr_core = { version = "0.2.3", default-features = false }
+cty = "0.2.1"
+default-uart-client = { path = "../default-uart-client", optional = true }
 kata-io = { path = "../kata-io" }
 kata-os-common = { path = "../../kata-os-common" }
 kata-shell = { path = "../kata-shell" }
-kata-uart-client = { path = "../kata-uart-client" }
+kata-uart-client = { path = "../kata-uart-client", optional = true }
 log = { version = "0.4", features = ["release_max_level_info"] }
+panic-halt = "0.2.0"
 
 [lib]
 name = "kata_debug_console"
diff --git a/apps/system/components/DebugConsole/kata-debug-console/src/run.rs b/apps/system/components/DebugConsole/kata-debug-console/src/run.rs
index 6ecfb4b..4c07952 100644
--- a/apps/system/components/DebugConsole/kata-debug-console/src/run.rs
+++ b/apps/system/components/DebugConsole/kata-debug-console/src/run.rs
@@ -24,8 +24,12 @@
 //! * kata_debug_console main entry point fn run()
 
 #![no_std]
+#![allow(clippy::missing_safety_doc)]
 
+use core::fmt::Write;
 use core::slice;
+use cpio::CpioNewcReader;
+use cstr_core::CStr;
 use kata_os_common::camkes::Camkes;
 use log::LevelFilter;
 
@@ -51,14 +55,82 @@ pub unsafe extern "C" fn pre_init() {
     CAMKES.pre_init(INIT_LOG_LEVEL, &mut HEAP_MEMORY);
 }
 
-/// Entry point for DebugConsole. Runs the shell with UART IO.
+// Returns a trait-compatible Tx based on the selected features.
+// NB: must use "return expr;" to avoid confusing the compiler.
+fn get_tx() -> impl kata_io::Write {
+    #[cfg(feature = "sparrow_uart_support")]
+    return kata_uart_client::Tx::new();
+
+    #[cfg(not(feature = "sparrow_uart_support"))]
+    return default_uart_client::Tx::new();
+}
+
+/// Console logging interface.
 #[no_mangle]
-pub extern "C" fn run() -> ! {
+pub unsafe extern "C" fn logger_log(level: u8, msg: *const cstr_core::c_char) {
+    use log::Level;
+    let l = match level {
+        x if x == Level::Error as u8 => Level::Error,
+        x if x == Level::Warn as u8 => Level::Warn,
+        x if x == Level::Info as u8 => Level::Info,
+        x if x == Level::Debug as u8 => Level::Debug,
+        _ => Level::Trace,
+    };
+    if l <= log::max_level() {
+        // TODO(sleffler): is the uart driver ok w/ multiple writers?
+        // TODO(sleffler): fallback to seL4_DebugPutChar?
+        let output: &mut dyn kata_io::Write = &mut get_tx();
+        let _ = writeln!(output, "{}", CStr::from_ptr(msg).to_str().unwrap());
+    }
+}
+
+// If the builtins archive includes an "autostart.repl" file it is run
+// through the shell with output sent either to the console or /dev/null
+// depending on the feature selection.
+#[cfg(feature = "autostart_support")]
+fn run_autostart_shell(cpio_archive_ref: &[u8]) {
+    const AUTOSTART_NAME: &str = "autostart.repl";
+
+    let mut autostart_script: Option<&[u8]> = None;
+    let reader = CpioNewcReader::new(cpio_archive_ref);
+    for e in reader {
+        if e.is_err() {
+            break;
+        }
+        let entry = e.unwrap();
+        if entry.name == AUTOSTART_NAME {
+            autostart_script = Some(entry.data);
+            break;
+        }
+    }
+    if let Some(script) = autostart_script {
+        // Rx data comes from the embedded script
+        // Tx data goes to either the uart or /dev/null
+        let mut rx = kata_io::BufReader::new(default_uart_client::Rx::new(script));
+        kata_shell::repl_eof(&mut get_tx(), &mut rx, cpio_archive_ref);
+    }
+}
+
+// Runs an interactive shell using the Sparrow UART.
+#[cfg(feature = "sparrow_uart_support")]
+fn run_sparrow_shell(cpio_archive_ref: &[u8]) -> ! {
     let mut tx = kata_uart_client::Tx::new();
     let mut rx = kata_io::BufReader::new(kata_uart_client::Rx::new());
+    kata_shell::repl(&mut tx, &mut rx, cpio_archive_ref);
+}
+
+/// Entry point for DebugConsole. Optionally runs an autostart script
+/// after which it runs an interactive shell with UART IO.
+#[no_mangle]
+pub extern "C" fn run() {
     let cpio_archive_ref = unsafe {
         // XXX want begin-end or begin+size instead of a fixed-size block
         slice::from_raw_parts(cpio_archive, 16777216)
     };
-    kata_shell::repl(&mut tx, &mut rx, cpio_archive_ref);
+
+    #[cfg(feature = "autostart_support")]
+    run_autostart_shell(cpio_archive_ref);
+
+    #[cfg(feature = "sparrow_uart_support")]
+    run_sparrow_shell(cpio_archive_ref);
 }
diff --git a/apps/system/components/DebugConsole/kata-shell/Cargo.toml b/apps/system/components/DebugConsole/kata-shell/Cargo.toml
index 78ce720..7a873e5 100644
--- a/apps/system/components/DebugConsole/kata-shell/Cargo.toml
+++ b/apps/system/components/DebugConsole/kata-shell/Cargo.toml
@@ -53,6 +53,7 @@ TEST_UART = []
 [dependencies]
 crc = { version = "1.4.0", default_features = false }
 cpio = { git = "https://github.com/rcore-os/cpio" }
+default-uart-client = { path = "../default-uart-client" }
 hashbrown = { version = "0.11", features = ["ahash-compile-time-rng"] }
 hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
 kata-io = { path = "../kata-io" }
diff --git a/apps/system/components/DebugConsole/kata-shell/src/lib.rs b/apps/system/components/DebugConsole/kata-shell/src/lib.rs
index 6163ca5..f60f7cd 100644
--- a/apps/system/components/DebugConsole/kata-shell/src/lib.rs
+++ b/apps/system/components/DebugConsole/kata-shell/src/lib.rs
@@ -114,8 +114,7 @@ type CmdFn = fn(
     builtin_cpio: &[u8],
 ) -> Result<(), CommandError>;
 
-/// Read-eval-print loop for the DebugConsole command line interface.
-pub fn repl<T: io::BufRead>(output: &mut dyn io::Write, input: &mut T, builtin_cpio: &[u8]) -> ! {
+fn get_cmds() -> HashMap<&'static str, CmdFn> {
     let mut cmds = HashMap::<&str, CmdFn>::new();
     cmds.extend([
         ("builtins", builtins_command as CmdFn),
@@ -129,6 +128,7 @@ pub fn repl<T: io::BufRead>(output: &mut dyn io::Write, input: &mut T, builtin_c
         ("mdebug", mdebug_command as CmdFn),
         ("mstats", mstats_command as CmdFn),
         ("ps", ps_command as CmdFn),
+        ("source", source_command as CmdFn),
         ("start", start_command as CmdFn),
         ("stop", stop_command as CmdFn),
         ("uninstall", uninstall_command as CmdFn),
@@ -152,35 +152,49 @@ pub fn repl<T: io::BufRead>(output: &mut dyn io::Write, input: &mut T, builtin_c
     #[cfg(feature = "TEST_UART")]
     test_uart::add_cmds(&mut cmds);
 
+    cmds
+}
+
+pub fn eval<T: io::BufRead>(
+    cmdline: &str,
+    cmds: &HashMap<&str, CmdFn>,
+    output: &mut dyn io::Write,
+    input: &mut T,
+    builtin_cpio: &[u8],
+) {
+    let mut args = cmdline.split_ascii_whitespace();
+    match args.next() {
+        Some("?") | Some("help") => {
+            let mut keys: Vec<&str> = cmds.keys().copied().collect();
+            keys.sort();
+            for k in keys {
+                let _ = writeln!(output, "{}", k);
+            }
+        }
+        Some(cmd) => {
+            let result = cmds.get(cmd).map_or_else(
+                || Err(CommandError::UnknownCommand),
+                |func| func(&mut args, input, output, builtin_cpio),
+            );
+            if let Err(e) = result {
+                let _ = writeln!(output, "{}", e);
+            };
+        }
+        None => {
+            let _ = output.write_str("\n");
+        }
+    }
+}
+
+/// Read-eval-print loop for the DebugConsole command line interface.
+pub fn repl<T: io::BufRead>(output: &mut dyn io::Write, input: &mut T, builtin_cpio: &[u8]) -> ! {
+    let cmds = get_cmds();
     let mut line_reader = LineReader::new();
     loop {
         const PROMPT: &str = "KATA> ";
         let _ = output.write_str(PROMPT);
         match line_reader.read_line(output, input) {
-            Ok(cmdline) => {
-                let mut args = cmdline.split_ascii_whitespace();
-                match args.next() {
-                    Some("?") | Some("help") => {
-                        let mut keys: Vec<&str> = cmds.keys().copied().collect();
-                        keys.sort();
-                        for k in keys {
-                            let _ = writeln!(output, "{}", k);
-                        }
-                    }
-                    Some(cmd) => {
-                        let result = cmds.get(cmd).map_or_else(
-                            || Err(CommandError::UnknownCommand),
-                            |func| func(&mut args, input, output, builtin_cpio),
-                        );
-                        if let Err(e) = result {
-                            let _ = writeln!(output, "{}", e);
-                        };
-                    }
-                    None => {
-                        let _ = output.write_str("\n");
-                    }
-                }
-            }
+            Ok(cmdline) => eval(cmdline, &cmds, output, input, builtin_cpio),
             Err(e) => {
                 let _ = writeln!(output, "\n{}", e);
             }
@@ -188,6 +202,48 @@ pub fn repl<T: io::BufRead>(output: &mut dyn io::Write, input: &mut T, builtin_c
     }
 }
 
+/// Stripped down repl for running automation scripts. Like repl but prints
+/// each cmd line and stops at EOF/error.
+pub fn repl_eof<T: io::BufRead>(output: &mut dyn io::Write, input: &mut T, builtin_cpio: &[u8]) {
+    let cmds = get_cmds();
+    let mut line_reader = LineReader::new();
+    while let Ok(cmdline) = line_reader.read_line(output, input) {
+        // NB: LineReader echo's input
+        eval(cmdline, &cmds, output, input, builtin_cpio);
+    }
+}
+
+/// Implements a "source" command that interprets commands from a file
+/// in the built-in cpio archive.
+fn source_command(
+    args: &mut dyn Iterator<Item = &str>,
+    _input: &mut dyn io::BufRead,
+    output: &mut dyn io::Write,
+    builtin_cpio: &[u8],
+) -> Result<(), CommandError> {
+    for script_name in args {
+        let mut script_data: Option<&[u8]> = None;
+        for e in CpioNewcReader::new(builtin_cpio) {
+            if e.is_err() {
+                writeln!(output, "cpio error")?;
+                break; // NB: iterator does not terminate on error
+            }
+            let entry = e.unwrap();
+            if entry.name == script_name {
+                script_data = Some(entry.data);
+                break;
+            }
+        }
+        if let Some(data) = script_data {
+            let mut script_input = kata_io::BufReader::new(default_uart_client::Rx::new(data));
+            repl_eof(output, &mut script_input, builtin_cpio);
+        } else {
+            writeln!(output, "{}: not found", script_name)?;
+        }
+    }
+    Ok(())
+}
+
 /// Implements a "builtins" command that lists the contents of the built-in cpio archive.
 fn builtins_command(
     _args: &mut dyn Iterator<Item = &str>,
diff --git a/apps/system/components/DebugConsole/kata-uart-client/Cargo.toml b/apps/system/components/DebugConsole/kata-uart-client/Cargo.toml
index 652dfa2..6675038 100644
--- a/apps/system/components/DebugConsole/kata-uart-client/Cargo.toml
+++ b/apps/system/components/DebugConsole/kata-uart-client/Cargo.toml
@@ -19,8 +19,5 @@ authors = ["Matt Harvey <mattharvey@google.com>"]
 edition = "2021"
 
 [dependencies]
-# Disable default so we don't pull in CString which requires an allocator
-cstr_core = { version = "0.2.3", default-features = false }
 cty = "0.2.1"
 kata-io = { path = "../kata-io" }
-log = { version = "0.4", features = ["release_max_level_info"] }
diff --git a/apps/system/components/DebugConsole/kata-uart-client/src/lib.rs b/apps/system/components/DebugConsole/kata-uart-client/src/lib.rs
index b5b9036..558e4ac 100644
--- a/apps/system/components/DebugConsole/kata-uart-client/src/lib.rs
+++ b/apps/system/components/DebugConsole/kata-uart-client/src/lib.rs
@@ -14,29 +14,8 @@
 
 #![no_std]
 
-use core::fmt::Write;
-use cstr_core::CStr;
 use kata_io as io;
 
-/// Console logging interface.
-#[no_mangle]
-#[allow(clippy::missing_safety_doc)]
-pub unsafe extern "C" fn logger_log(level: u8, msg: *const cstr_core::c_char) {
-    use log::Level;
-    let l = match level {
-        x if x == Level::Error as u8 => Level::Error,
-        x if x == Level::Warn as u8 => Level::Warn,
-        x if x == Level::Info as u8 => Level::Info,
-        x if x == Level::Debug as u8 => Level::Debug,
-        _ => Level::Trace,
-    };
-    if l <= log::max_level() {
-        // TODO(sleffler): is the uart driver ok w/ multiple writers?
-        let output: &mut dyn io::Write = &mut self::Tx::new();
-        let _ = writeln!(output, "{}", CStr::from_ptr(msg).to_str().unwrap());
-    }
-}
-
 const DATAPORT_SIZE: usize = 4096;
 
 pub struct Rx {