diff --git a/Cargo.lock b/Cargo.lock
index 13f57e14..f3151a0a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3598,7 +3598,7 @@ dependencies = [
 
 [[package]]
 name = "trunk"
-version = "0.20.3"
+version = "0.20.4"
 dependencies = [
  "ansi_term",
  "anyhow",
diff --git a/Cargo.toml b/Cargo.toml
index fb1a1448..7a3f25d2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "trunk"
-version = "0.20.3"
+version = "0.20.4"
 edition = "2021"
 description = "Build, bundle & ship your Rust WASM application to the web."
 license = "MIT/Apache-2.0"
diff --git a/Trunk.toml b/Trunk.toml
index 300dae55..7ade5977 100644
--- a/Trunk.toml
+++ b/Trunk.toml
@@ -126,6 +126,14 @@ stage = "build"
 command = "sh"
 command_arguments = ["-c", "echo Staging directory: $TRUNK_STAGING_DIR"]
 
+[hooks.windows]
+# This shows how to create OS-specific overrides for hooks.
+# This will override the hook directly above it.
+# For overrides, only the `command` and `command_arguments` keys must be specified.
+# Valid values are `windows`, `macos`, and `linux`.
+command = "cmd"
+command_arguments = ["/c", "echo Staging directory: %TRUNK_STAGING_DIR%"]
+
 [[hooks]]
 # This hook example shows how command_arguments defaults to an empty list when absent. It also uses
 # the post_build stage, meaning it executes after the rest of the build is complete, just before
diff --git a/site/content/assets.md b/site/content/assets.md
index 20a7b28a..7d87ce82 100644
--- a/site/content/assets.md
+++ b/site/content/assets.md
@@ -183,6 +183,10 @@ All hooks are executed using the same `stdin` and `stdout` as trunk. The executa
   - `TRUNK_DIST_DIR`: the full path of the Trunk dist directory.
   - `TRUNK_PUBLIC_URL`: the configured public URL for Trunk.
 
+## OS-specific overrides
+
+Often times you will want to perform the same build step on different OSes, requiring different commands. A typical example of this is using the `sh` command on Linux, but `cmd` on Windows. To accomodate this, you can optionally create OS-specific overrides for each hook. To do this, specify the default hook, then directly below it create a `[hooks.<os>]` entry where `<os>` can be one of `windows`, `macos`, or `linux`. Within this entry you must specify only the `command` and `command_argumnets` keys. You may provide multiple overrides for each hook. i.e. One for `windows`, one for `macos`, and one for `linux`.
+
 # Auto-Reload
 
 As of `v0.14.0`, Trunk now ships with the ability to automatically reload your web app as the Trunk build pipeline completes.
diff --git a/src/config/models/hook.rs b/src/config/models/hook.rs
index 872a15f4..89f8ec40 100644
--- a/src/config/models/hook.rs
+++ b/src/config/models/hook.rs
@@ -7,6 +7,67 @@ use serde::Deserialize;
 pub struct ConfigOptsHook {
     /// The stage in the build process to execute this hook.
     pub stage: PipelineStage,
+    /// The command to run for this hook.
+    command: String,
+    /// Any arguments to pass to the command.
+    #[serde(default)]
+    command_arguments: Vec<String>,
+    /// Overrides
+    #[serde(default, flatten)]
+    overrides: ConfigOptsHookOverrride,
+}
+
+impl ConfigOptsHook {
+    pub fn command(&self) -> &String {
+        if cfg!(target_os = "windows") {
+            if let Some(cfg) = self.overrides.windows.as_ref() {
+                return &cfg.command;
+            }
+        } else if cfg!(target_os = "macos") {
+            if let Some(cfg) = self.overrides.macos.as_ref() {
+                return &cfg.command;
+            }
+        } else if cfg!(target_os = "linux") {
+            if let Some(cfg) = self.overrides.linux.as_ref() {
+                return &cfg.command;
+            }
+        }
+
+        &self.command
+    }
+
+    pub fn command_arguments(&self) -> &Vec<String> {
+        if cfg!(target_os = "windows") {
+            if let Some(cfg) = self.overrides.windows.as_ref() {
+                return &cfg.command_arguments;
+            }
+        } else if cfg!(target_os = "macos") {
+            if let Some(cfg) = self.overrides.macos.as_ref() {
+                return &cfg.command_arguments;
+            }
+        } else if cfg!(target_os = "linux") {
+            if let Some(cfg) = self.overrides.linux.as_ref() {
+                return &cfg.command_arguments;
+            }
+        }
+
+        &self.command_arguments
+    }
+}
+
+/// Config options for build system hooks.
+#[derive(Clone, Debug, Deserialize, Default)]
+#[serde(rename_all = "snake_case")]
+pub struct ConfigOptsHookOverrride {
+    pub windows: Option<ConfigOptsHookOs>,
+    pub macos: Option<ConfigOptsHookOs>,
+    pub linux: Option<ConfigOptsHookOs>,
+}
+
+/// OS-specific overrides.
+#[derive(Clone, Debug, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub struct ConfigOptsHookOs {
     /// The command to run for this hook.
     pub command: String,
     /// Any arguments to pass to the command.
diff --git a/src/hooks.rs b/src/hooks.rs
index f5850fa5..94147b06 100644
--- a/src/hooks.rs
+++ b/src/hooks.rs
@@ -19,9 +19,9 @@ pub fn spawn_hooks(cfg: Arc<RtcBuild>, stage: PipelineStage) -> HookHandles {
         .iter()
         .filter(|hook_cfg| hook_cfg.stage == stage)
         .map(|hook_cfg| {
-            let mut command = Command::new(&hook_cfg.command);
+            let mut command = Command::new(hook_cfg.command());
             command
-                .args(&hook_cfg.command_arguments)
+                .args(hook_cfg.command_arguments())
                 .stdout(Stdio::inherit())
                 .stderr(Stdio::inherit())
                 .env("TRUNK_PROFILE", if cfg.release { "release" } else { "debug" })
@@ -31,9 +31,9 @@ pub fn spawn_hooks(cfg: Arc<RtcBuild>, stage: PipelineStage) -> HookHandles {
                 .env("TRUNK_DIST_DIR", &cfg.final_dist)
                 .env("TRUNK_PUBLIC_URL", &cfg.public_url);
 
-            tracing::info!(command_arguments = ?hook_cfg.command_arguments, "spawned hook {}", hook_cfg.command);
+            tracing::info!(command_arguments = ?hook_cfg.command_arguments(), "spawned hook {}", hook_cfg.command());
 
-            let command_name = hook_cfg.command.clone();
+            let command_name = hook_cfg.command().clone();
             tracing::info!(?stage, command = %command_name, "spawning hook");
             tokio::spawn(async move {
                 let status = command