diff --git a/CHANGELOG.md b/CHANGELOG.md
index b461f10..0653f15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   part:TestMethod("Hello", "world!")
   ```
 
+### Changed
+
+- Stdio options when using `process.spawn` can now be set with more granularity, allowing stderr & stdout to be disabled individually and completely to improve memory usage when they are not being used.
+
 ## `0.7.8` - October 5th, 2023
 
 ### Added
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 9a80c9d..cb7daee 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -84,6 +84,7 @@ impl Cli {
         // This will also exit early and not run anything else
         if self.list {
             let sorted_relative = find_lune_scripts(false).await.map(sort_lune_scripts);
+
             let sorted_home_dir = find_lune_scripts(true).await.map(sort_lune_scripts);
             if sorted_relative.is_err() && sorted_home_dir.is_err() {
                 eprintln!("{}", sorted_relative.unwrap_err());
diff --git a/src/cli/utils/files.rs b/src/cli/utils/files.rs
index 3bbf4a0..9b9b1ca 100644
--- a/src/cli/utils/files.rs
+++ b/src/cli/utils/files.rs
@@ -151,6 +151,7 @@ pub fn discover_script_path_including_lune_dirs(path: &str) -> Result<PathBuf> {
                 .or_else(|_| discover_script_path(format!(".lune{MAIN_SEPARATOR}{path}"), false))
                 .or_else(|_| discover_script_path(format!("lune{MAIN_SEPARATOR}{path}"), true))
                 .or_else(|_| discover_script_path(format!(".lune{MAIN_SEPARATOR}{path}"), true));
+
             match res {
                 // NOTE: The first error message is generally more
                 // descriptive than the ones for the lune subfolders
diff --git a/src/cli/utils/listing.rs b/src/cli/utils/listing.rs
index 63fd3a0..84d7e13 100644
--- a/src/cli/utils/listing.rs
+++ b/src/cli/utils/listing.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::match_same_arms)]
+
 use std::{cmp::Ordering, ffi::OsStr, fmt::Write as _, path::PathBuf};
 
 use anyhow::{bail, Result};
@@ -6,7 +8,7 @@ use directories::UserDirs;
 use once_cell::sync::Lazy;
 use tokio::{fs, io};
 
-use super::files::parse_lune_description_from_file;
+use super::files::{discover_script_path, parse_lune_description_from_file};
 
 pub static COLOR_BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue());
 pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
@@ -28,16 +30,44 @@ pub async fn find_lune_scripts(in_home_dir: bool) -> Result<Vec<(String, String)
                 let meta = entry.metadata().await?;
                 if meta.is_file() {
                     let contents = fs::read(entry.path()).await?;
+                    files.push((entry, meta, contents));
+                } else if meta.is_dir() {
+                    let entry_path_os = entry.path();
+
+                    let Some(dir_path) = entry_path_os.to_str() else {
+                        bail!("Failed to cast path to string slice.")
+                    };
+
+                    let init_file_path = match discover_script_path(dir_path, in_home_dir) {
+                        Ok(init_file) => init_file,
+                        _ => continue,
+                    };
+
+                    let contents = fs::read(init_file_path).await?;
+
                     files.push((entry, meta, contents));
                 }
             }
             let parsed: Vec<_> = files
                 .iter()
                 .filter(|(entry, _, _)| {
-                    matches!(
+                    let mut is_match = matches!(
                         entry.path().extension().and_then(OsStr::to_str),
                         Some("lua" | "luau")
-                    )
+                    );
+
+                    // If the entry is not a lua or luau file, and is a directory,
+                    // then we check if it contains a init.lua(u), and if it does,
+                    // we include it in our Vec
+                    if !is_match
+                        && entry.path().is_dir()
+                        && (entry.path().join("init.lua").exists()
+                            || entry.path().join("init.luau").exists())
+                    {
+                        is_match = true;
+                    }
+
+                    is_match
                 })
                 .map(|(entry, _, contents)| {
                     let contents_str = String::from_utf8_lossy(contents);
diff --git a/src/lune/builtins/process/mod.rs b/src/lune/builtins/process/mod.rs
index 98309c3..154a21f 100644
--- a/src/lune/builtins/process/mod.rs
+++ b/src/lune/builtins/process/mod.rs
@@ -1,7 +1,7 @@
 use std::{
     env::{self, consts},
     path,
-    process::{ExitStatus, Stdio},
+    process::Stdio,
 };
 
 use dunce::canonicalize;
@@ -13,12 +13,12 @@ use crate::lune::{scheduler::Scheduler, util::TableBuilder};
 
 mod tee_writer;
 
-mod pipe_inherit;
-use pipe_inherit::pipe_and_inherit_child_process_stdio;
-
 mod options;
 use options::ProcessSpawnOptions;
 
+mod wait_for_child;
+use wait_for_child::{wait_for_child, WaitForChildResult};
+
 const PROCESS_EXIT_IMPL_LUA: &str = r#"
 exit(...)
 yield()
@@ -169,21 +169,26 @@ async fn process_spawn(
         runtime place it on a different thread if possible / necessary
 
         Note that we have to use our scheduler here, we can't
-        use anything like tokio::task::spawn because our lua
-        scheduler will not drive those futures to completion
+        be using tokio::task::spawn directly because our lua
+        scheduler would not drive those futures to completion
     */
     let sched = lua
         .app_data_ref::<&Scheduler>()
         .expect("Lua struct is missing scheduler");
 
-    let (status, stdout, stderr) = sched
+    let res = sched
         .spawn(spawn_command(program, args, options))
         .await
         .expect("Failed to receive result of spawned process")?;
 
-    // NOTE: If an exit code was not given by the child process,
-    // we default to 1 if it yielded any error output, otherwise 0
-    let code = status.code().unwrap_or(match stderr.is_empty() {
+    /*
+        NOTE: If an exit code was not given by the child process,
+        we default to 1 if it yielded any error output, otherwise 0
+
+        An exit code may be missing if the process was terminated by
+        some external signal, which is the only time we use this default
+    */
+    let code = res.status.code().unwrap_or(match res.stderr.is_empty() {
         true => 0,
         false => 1,
     });
@@ -192,8 +197,8 @@ async fn process_spawn(
     TableBuilder::new(lua)?
         .with_value("ok", code == 0)?
         .with_value("code", code)?
-        .with_value("stdout", lua.create_string(&stdout)?)?
-        .with_value("stderr", lua.create_string(&stderr)?)?
+        .with_value("stdout", lua.create_string(&res.stdout)?)?
+        .with_value("stderr", lua.create_string(&res.stderr)?)?
         .build_readonly()
 }
 
@@ -201,9 +206,10 @@ async fn spawn_command(
     program: String,
     args: Option<Vec<String>>,
     mut options: ProcessSpawnOptions,
-) -> LuaResult<(ExitStatus, Vec<u8>, Vec<u8>)> {
-    let inherit_stdio = options.inherit_stdio;
-    let stdin = options.stdin.take();
+) -> LuaResult<WaitForChildResult> {
+    let stdout = options.stdio.stdout;
+    let stderr = options.stdio.stderr;
+    let stdin = options.stdio.stdin.take();
 
     let mut child = options
         .into_command(program, args)
@@ -211,20 +217,14 @@ async fn spawn_command(
             true => Stdio::piped(),
             false => Stdio::null(),
         })
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
+        .stdout(stdout.as_stdio())
+        .stderr(stderr.as_stdio())
         .spawn()?;
 
-    // If the stdin option was provided, we write that to the child
     if let Some(stdin) = stdin {
         let mut child_stdin = child.stdin.take().unwrap();
         child_stdin.write_all(&stdin).await.into_lua_err()?;
     }
 
-    if inherit_stdio {
-        pipe_and_inherit_child_process_stdio(child).await
-    } else {
-        let output = child.wait_with_output().await?;
-        Ok((output.status, output.stdout, output.stderr))
-    }
+    wait_for_child(child, stdout, stderr).await
 }
diff --git a/src/lune/builtins/process/options/kind.rs b/src/lune/builtins/process/options/kind.rs
new file mode 100644
index 0000000..3e0f39c
--- /dev/null
+++ b/src/lune/builtins/process/options/kind.rs
@@ -0,0 +1,80 @@
+use std::{fmt, process::Stdio, str::FromStr};
+
+use itertools::Itertools;
+use mlua::prelude::*;
+
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub enum ProcessSpawnOptionsStdioKind {
+    // TODO: We need better more obvious names
+    // for these, but that is a breaking change
+    #[default]
+    Default,
+    Forward,
+    Inherit,
+    None,
+}
+
+impl ProcessSpawnOptionsStdioKind {
+    pub fn all() -> &'static [Self] {
+        &[Self::Default, Self::Forward, Self::Inherit, Self::None]
+    }
+
+    pub fn as_stdio(self) -> Stdio {
+        match self {
+            Self::None => Stdio::null(),
+            Self::Forward => Stdio::inherit(),
+            _ => Stdio::piped(),
+        }
+    }
+}
+
+impl fmt::Display for ProcessSpawnOptionsStdioKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let s = match *self {
+            Self::Default => "default",
+            Self::Forward => "forward",
+            Self::Inherit => "inherit",
+            Self::None => "none",
+        };
+        f.write_str(s)
+    }
+}
+
+impl FromStr for ProcessSpawnOptionsStdioKind {
+    type Err = LuaError;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s.trim().to_ascii_lowercase().as_str() {
+            "default" => Self::Default,
+            "forward" => Self::Forward,
+            "inherit" => Self::Inherit,
+            "none" => Self::None,
+            _ => {
+                return Err(LuaError::RuntimeError(format!(
+                    "Invalid spawn options stdio kind - got '{}', expected one of {}",
+                    s,
+                    ProcessSpawnOptionsStdioKind::all()
+                        .iter()
+                        .map(|k| format!("'{k}'"))
+                        .join(", ")
+                )))
+            }
+        })
+    }
+}
+
+impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdioKind {
+    fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
+        match value {
+            LuaValue::Nil => Ok(Self::default()),
+            LuaValue::String(s) => s.to_str()?.parse(),
+            _ => Err(LuaError::FromLuaConversionError {
+                from: value.type_name(),
+                to: "ProcessSpawnOptionsStdioKind",
+                message: Some(format!(
+                    "Invalid spawn options stdio kind - expected string, got {}",
+                    value.type_name()
+                )),
+            }),
+        }
+    }
+}
diff --git a/src/lune/builtins/process/options.rs b/src/lune/builtins/process/options/mod.rs
similarity index 80%
rename from src/lune/builtins/process/options.rs
rename to src/lune/builtins/process/options/mod.rs
index 4c4fc73..d37f844 100644
--- a/src/lune/builtins/process/options.rs
+++ b/src/lune/builtins/process/options/mod.rs
@@ -8,13 +8,18 @@ use directories::UserDirs;
 use mlua::prelude::*;
 use tokio::process::Command;
 
+mod kind;
+mod stdio;
+
+pub(super) use kind::*;
+pub(super) use stdio::*;
+
 #[derive(Debug, Clone, Default)]
-pub struct ProcessSpawnOptions {
-    pub(crate) cwd: Option<PathBuf>,
-    pub(crate) envs: HashMap<String, String>,
-    pub(crate) shell: Option<String>,
-    pub(crate) inherit_stdio: bool,
-    pub(crate) stdin: Option<Vec<u8>>,
+pub(super) struct ProcessSpawnOptions {
+    pub cwd: Option<PathBuf>,
+    pub envs: HashMap<String, String>,
+    pub shell: Option<String>,
+    pub stdio: ProcessSpawnOptionsStdio,
 }
 
 impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
@@ -112,34 +117,14 @@ impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
         }
 
         /*
-            If we got options for stdio handling, make sure its one of the constant values
-        */
-        match value.get("stdio")? {
-            LuaValue::Nil => {}
-            LuaValue::String(s) => match s.to_str()? {
-                "inherit" => this.inherit_stdio = true,
-                "default" => this.inherit_stdio = false,
-                _ => {
-                    return Err(LuaError::RuntimeError(format!(
-                    "Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'",
-                    s.to_string_lossy()
-                )))
-                }
-            },
-            value => {
-                return Err(LuaError::RuntimeError(format!(
-                    "Invalid type for option 'stdio' - expected 'string', got '{}'",
-                    value.type_name()
-                )))
-            }
-        }
-
-        /*
-            If we have stdin contents, we need to pass those to the child process
+            If we got options for stdio handling, parse those as well - note that
+            we accept a separate "stdin" value here for compatibility with older
+            scripts, but the user should preferrably pass it in the stdio table
         */
+        this.stdio = value.get("stdio")?;
         match value.get("stdin")? {
             LuaValue::Nil => {}
-            LuaValue::String(s) => this.stdin = Some(s.as_bytes().to_vec()),
+            LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()),
             value => {
                 return Err(LuaError::RuntimeError(format!(
                     "Invalid type for option 'stdin' - expected 'string', got '{}'",
diff --git a/src/lune/builtins/process/options/stdio.rs b/src/lune/builtins/process/options/stdio.rs
new file mode 100644
index 0000000..4c12ff4
--- /dev/null
+++ b/src/lune/builtins/process/options/stdio.rs
@@ -0,0 +1,56 @@
+use mlua::prelude::*;
+
+use super::kind::ProcessSpawnOptionsStdioKind;
+
+#[derive(Debug, Clone, Default)]
+pub struct ProcessSpawnOptionsStdio {
+    pub stdout: ProcessSpawnOptionsStdioKind,
+    pub stderr: ProcessSpawnOptionsStdioKind,
+    pub stdin: Option<Vec<u8>>,
+}
+
+impl From<ProcessSpawnOptionsStdioKind> for ProcessSpawnOptionsStdio {
+    fn from(value: ProcessSpawnOptionsStdioKind) -> Self {
+        Self {
+            stdout: value,
+            stderr: value,
+            ..Default::default()
+        }
+    }
+}
+
+impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdio {
+    fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
+        match value {
+            LuaValue::Nil => Ok(Self::default()),
+            LuaValue::String(s) => {
+                Ok(ProcessSpawnOptionsStdioKind::from_lua(LuaValue::String(s), lua)?.into())
+            }
+            LuaValue::Table(t) => {
+                let mut this = Self::default();
+
+                if let Some(stdin) = t.get("stdin")? {
+                    this.stdin = stdin;
+                }
+
+                if let Some(stdout) = t.get("stdout")? {
+                    this.stdout = stdout;
+                }
+
+                if let Some(stderr) = t.get("stderr")? {
+                    this.stderr = stderr;
+                }
+
+                Ok(this)
+            }
+            _ => Err(LuaError::FromLuaConversionError {
+                from: value.type_name(),
+                to: "ProcessSpawnOptionsStdio",
+                message: Some(format!(
+                    "Invalid spawn options stdio - expected string or table, got {}",
+                    value.type_name()
+                )),
+            }),
+        }
+    }
+}
diff --git a/src/lune/builtins/process/pipe_inherit.rs b/src/lune/builtins/process/pipe_inherit.rs
deleted file mode 100644
index 0e4b9a3..0000000
--- a/src/lune/builtins/process/pipe_inherit.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use std::process::ExitStatus;
-
-use mlua::prelude::*;
-use tokio::{io, process::Child, task};
-
-use super::tee_writer::AsyncTeeWriter;
-
-pub async fn pipe_and_inherit_child_process_stdio(
-    mut child: Child,
-) -> LuaResult<(ExitStatus, Vec<u8>, Vec<u8>)> {
-    let mut child_stdout = child.stdout.take().unwrap();
-    let mut child_stderr = child.stderr.take().unwrap();
-
-    /*
-        NOTE: We do not need to register these
-        independent tasks spawning in the scheduler
-
-        This function is only used by `process.spawn` which in
-        turn registers a task with the scheduler that awaits this
-    */
-
-    let stdout_thread = task::spawn(async move {
-        let mut stdout = io::stdout();
-        let mut tee = AsyncTeeWriter::new(&mut stdout);
-
-        io::copy(&mut child_stdout, &mut tee).await.into_lua_err()?;
-
-        Ok::<_, LuaError>(tee.into_vec())
-    });
-
-    let stderr_thread = task::spawn(async move {
-        let mut stderr = io::stderr();
-        let mut tee = AsyncTeeWriter::new(&mut stderr);
-
-        io::copy(&mut child_stderr, &mut tee).await.into_lua_err()?;
-
-        Ok::<_, LuaError>(tee.into_vec())
-    });
-
-    let status = child.wait().await.expect("Child process failed to start");
-
-    let stdout_buffer = stdout_thread.await.expect("Tee writer for stdout errored");
-    let stderr_buffer = stderr_thread.await.expect("Tee writer for stderr errored");
-
-    Ok::<_, LuaError>((status, stdout_buffer?, stderr_buffer?))
-}
diff --git a/src/lune/builtins/process/wait_for_child.rs b/src/lune/builtins/process/wait_for_child.rs
new file mode 100644
index 0000000..f126efe
--- /dev/null
+++ b/src/lune/builtins/process/wait_for_child.rs
@@ -0,0 +1,74 @@
+use std::process::ExitStatus;
+
+use mlua::prelude::*;
+use tokio::{
+    io::{self, AsyncRead, AsyncReadExt},
+    process::Child,
+    task,
+};
+
+use super::{options::ProcessSpawnOptionsStdioKind, tee_writer::AsyncTeeWriter};
+
+#[derive(Debug, Clone)]
+pub(super) struct WaitForChildResult {
+    pub status: ExitStatus,
+    pub stdout: Vec<u8>,
+    pub stderr: Vec<u8>,
+}
+
+async fn read_with_stdio_kind<R>(
+    read_from: Option<R>,
+    kind: ProcessSpawnOptionsStdioKind,
+) -> LuaResult<Vec<u8>>
+where
+    R: AsyncRead + Unpin,
+{
+    Ok(match kind {
+        ProcessSpawnOptionsStdioKind::None => Vec::new(),
+        ProcessSpawnOptionsStdioKind::Forward => Vec::new(),
+        ProcessSpawnOptionsStdioKind::Default => {
+            let mut read_from =
+                read_from.expect("read_from must be Some when stdio kind is Default");
+
+            let mut buffer = Vec::new();
+
+            read_from.read_to_end(&mut buffer).await.into_lua_err()?;
+
+            buffer
+        }
+        ProcessSpawnOptionsStdioKind::Inherit => {
+            let mut read_from =
+                read_from.expect("read_from must be Some when stdio kind is Inherit");
+
+            let mut stdout = io::stdout();
+            let mut tee = AsyncTeeWriter::new(&mut stdout);
+
+            io::copy(&mut read_from, &mut tee).await.into_lua_err()?;
+
+            tee.into_vec()
+        }
+    })
+}
+
+pub(super) async fn wait_for_child(
+    mut child: Child,
+    stdout_kind: ProcessSpawnOptionsStdioKind,
+    stderr_kind: ProcessSpawnOptionsStdioKind,
+) -> LuaResult<WaitForChildResult> {
+    let stdout_opt = child.stdout.take();
+    let stderr_opt = child.stderr.take();
+
+    let stdout_task = task::spawn(read_with_stdio_kind(stdout_opt, stdout_kind));
+    let stderr_task = task::spawn(read_with_stdio_kind(stderr_opt, stderr_kind));
+
+    let status = child.wait().await.expect("Child process failed to start");
+
+    let stdout_buffer = stdout_task.await.into_lua_err()??;
+    let stderr_buffer = stderr_task.await.into_lua_err()??;
+
+    Ok(WaitForChildResult {
+        status,
+        stdout: stdout_buffer,
+        stderr: stderr_buffer,
+    })
+}
diff --git a/types/process.luau b/types/process.luau
index 131b802..7b82052 100644
--- a/types/process.luau
+++ b/types/process.luau
@@ -1,7 +1,12 @@
 export type OS = "linux" | "macos" | "windows"
 export type Arch = "x86_64" | "aarch64"
 
-export type SpawnOptionsStdio = "inherit" | "default"
+export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
+export type SpawnOptionsStdio = {
+	stdout: SpawnOptionsStdioKind?,
+	stderr: SpawnOptionsStdioKind?,
+	stdin: string?,
+}
 
 --[=[
 	@interface SpawnOptions
@@ -12,15 +17,15 @@ export type SpawnOptionsStdio = "inherit" | "default"
 	* `cwd` - The current working directory for the process
 	* `env` - Extra environment variables to give to the process
 	* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
-	* `stdio` - How to treat output and error streams from the child process - set to "inherit" to pass output and error streams to the current process
+	* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
 	* `stdin` - Optional standard input to pass to spawned child process
 ]=]
 export type SpawnOptions = {
 	cwd: string?,
 	env: { [string]: string }?,
 	shell: (boolean | string)?,
-	stdio: SpawnOptionsStdio?,
-	stdin: string?,
+	stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
+	stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
 }
 
 --[=[