diff --git a/cmd/app.go b/cmd/app.go
index 504bde5fcf275efb70765e15faf4f5dd3d24fc37..f726587e7ca4104716322bddb23e3abc0d844e93 100644
--- a/cmd/app.go
+++ b/cmd/app.go
@@ -9,11 +9,6 @@ import (
 	"os"
 )
 
-var (
-	defaultClientRootConfigFile = "/etc/ntfy/client.yml"
-	defaultClientUserConfigFile = "~/.config/ntfy/client.yml"
-)
-
 const (
 	categoryClient = "Client commands"
 	categoryServer = "Server commands"
diff --git a/cmd/publish.go b/cmd/publish.go
index 34d7bf70df2d7c3e016a6fba2b6adfbbfcfc1226..728185e0bae39042936460b17ec79ce8c25f901a 100644
--- a/cmd/publish.go
+++ b/cmd/publish.go
@@ -63,8 +63,7 @@ Examples:
 Please also check out the docs on publishing messages. Especially for the --tags and --delay options, 
 it has incredibly useful information: https://ntfy.sh/docs/publish/.
 
-The default config file for all client commands is /etc/ntfy/client.yml (if root user),
-or ~/.config/ntfy/client.yml for all other users.`,
+` + defaultClientConfigFileDescriptionSuffix,
 }
 
 func execPublish(c *cli.Context) error {
diff --git a/cmd/subscribe.go b/cmd/subscribe.go
index 95ad02a4478365eb2238d96e14a7d569cb4ed13e..035f4abeb8ab5df3d603e242d02174234d32d13a 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -6,10 +6,7 @@ import (
 	"github.com/urfave/cli/v2"
 	"heckel.io/ntfy/client"
 	"heckel.io/ntfy/util"
-	"log"
 	"os"
-	"os/exec"
-	"os/user"
 	"strings"
 )
 
@@ -64,19 +61,17 @@ ntfy subscribe TOPIC COMMAND
 
   Examples:
     ntfy sub mytopic 'notify-send "$m"'    # Execute command for incoming messages
-    ntfy sub topic1 /my/script.sh          # Execute script for incoming messages
+    ntfy sub topic1 myscript.sh            # Execute script for incoming messages
 
 ntfy subscribe --from-config
-  Service mode (used in ntfy-client.service). This reads the config file (/etc/ntfy/client.yml 
-  or ~/.config/ntfy/client.yml) and sets up subscriptions for every topic in the "subscribe:" 
-  block (see config file).
+  Service mode (used in ntfy-client.service). This reads the config file and sets up 
+  subscriptions for every topic in the "subscribe:" block (see config file).
 
   Examples: 
     ntfy sub --from-config                           # Read topics from config file
-    ntfy sub --config=/my/client.yml --from-config   # Read topics from alternate config file
+    ntfy sub --config=myclient.yml --from-config     # Read topics from alternate config file
 
-The default config file for all client commands is /etc/ntfy/client.yml (if root user),
-or ~/.config/ntfy/client.yml for all other users.`,
+` + defaultClientConfigFileDescriptionSuffix,
 }
 
 func execSubscribe(c *cli.Context) error {
@@ -160,8 +155,8 @@ func doPollSingle(c *cli.Context, cl *client.Client, topic, command string, opti
 }
 
 func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
-	commands := make(map[string]string) // Subscription ID -> command
-	for _, s := range conf.Subscribe {  // May be nil
+	cmds := make(map[string]string)    // Subscription ID -> command
+	for _, s := range conf.Subscribe { // May be nil
 		topicOptions := append(make([]client.SubscribeOption, 0), options...)
 		for filter, value := range s.If {
 			topicOptions = append(topicOptions, client.WithFilter(filter, value))
@@ -170,18 +165,18 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
 			topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
 		}
 		subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
-		commands[subscriptionID] = s.Command
+		cmds[subscriptionID] = s.Command
 	}
 	if topic != "" {
 		subscriptionID := cl.Subscribe(topic, options...)
-		commands[subscriptionID] = command
+		cmds[subscriptionID] = command
 	}
 	for m := range cl.Messages {
-		command, ok := commands[m.SubscriptionID]
+		cmd, ok := cmds[m.SubscriptionID]
 		if !ok {
 			continue
 		}
-		printMessageOrRunCommand(c, m, command)
+		printMessageOrRunCommand(c, m, cmd)
 	}
 	return nil
 }
@@ -200,33 +195,6 @@ func runCommand(c *cli.Context, command string, m *client.Message) {
 	}
 }
 
-func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
-	scriptFile, err := createTmpScript(command)
-	if err != nil {
-		return err
-	}
-	defer os.Remove(scriptFile)
-	verbose := c.Bool("verbose")
-	if verbose {
-		log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
-	}
-	cmd := exec.Command("sh", "-c", scriptFile)
-	cmd.Stdin = c.App.Reader
-	cmd.Stdout = c.App.Writer
-	cmd.Stderr = c.App.ErrWriter
-	cmd.Env = envVars(m)
-	return cmd.Run()
-}
-
-func createTmpScript(command string) (string, error) {
-	scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
-	script := fmt.Sprintf("#!/bin/sh\n%s", command)
-	if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
-		return "", err
-	}
-	return scriptFile, nil
-}
-
 func envVars(m *client.Message) []string {
 	env := os.Environ()
 	env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
@@ -253,11 +221,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
 	if filename != "" {
 		return client.LoadConfig(filename)
 	}
-	u, _ := user.Current()
-	configFile := defaultClientRootConfigFile
-	if u.Uid != "0" {
-		configFile = util.ExpandHome(defaultClientUserConfigFile)
-	}
+	configFile := defaultConfigFile()
 	if s, _ := os.Stat(configFile); s != nil {
 		return client.LoadConfig(configFile)
 	}
diff --git a/cmd/subscribe_linux.go b/cmd/subscribe_linux.go
new file mode 100644
index 0000000000000000000000000000000000000000..2b55f8062fdf2e8d9243efe61dd26ca77ba9581e
--- /dev/null
+++ b/cmd/subscribe_linux.go
@@ -0,0 +1,57 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/urfave/cli/v2"
+	"heckel.io/ntfy/client"
+	"heckel.io/ntfy/util"
+	"log"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+)
+
+const (
+	defaultClientRootConfigFile              = "/etc/ntfy/client.yml"
+	defaultClientUserConfigFileRelative      = "ntfy/client.yml"
+	defaultClientConfigFileDescriptionSuffix = `The default config file for all client commands is /etc/ntfy/client.yml (if root user),
+or ~/.config/ntfy/client.yml for all other users.`
+)
+
+func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
+	scriptFile, err := createTmpScript(command)
+	if err != nil {
+		return err
+	}
+	defer os.Remove(scriptFile)
+	verbose := c.Bool("verbose")
+	if verbose {
+		log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
+	}
+	cmd := exec.Command("sh", "-c", scriptFile)
+	cmd.Stdin = c.App.Reader
+	cmd.Stdout = c.App.Writer
+	cmd.Stderr = c.App.ErrWriter
+	cmd.Env = envVars(m)
+	return cmd.Run()
+}
+
+func createTmpScript(command string) (string, error) {
+	scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
+	script := fmt.Sprintf("#!/bin/sh\n%s", command)
+	if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
+		return "", err
+	}
+	return scriptFile, nil
+}
+
+func defaultConfigFile() string {
+	u, _ := user.Current()
+	configFile := defaultClientRootConfigFile
+	if u.Uid != "0" {
+		homeDir, _ := os.UserConfigDir()
+		return filepath.Join(homeDir, defaultClientUserConfigFileRelative)
+	}
+	return configFile
+}
diff --git a/cmd/subscribe_windows.go b/cmd/subscribe_windows.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd441baeb1116cb361395a32ed434370a128f9db
--- /dev/null
+++ b/cmd/subscribe_windows.go
@@ -0,0 +1,48 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/urfave/cli/v2"
+	"heckel.io/ntfy/client"
+	"heckel.io/ntfy/util"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+const (
+	defaultClientUserConfigFileRelative      = "ntfy\\client.yml"
+	defaultClientConfigFileDescriptionSuffix = `The default config file for all client commands is %AppData%\ntfy\client.yml.`
+)
+
+func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
+	scriptFile, err := createTmpScript(command)
+	if err != nil {
+		return err
+	}
+	defer os.Remove(scriptFile)
+	verbose := c.Bool("verbose")
+	if verbose {
+		log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
+	}
+	cmd := exec.Command("cmd.exe", "/Q", "/C", scriptFile)
+	cmd.Stdin = c.App.Reader
+	cmd.Stdout = c.App.Writer
+	cmd.Stderr = c.App.ErrWriter
+	cmd.Env = envVars(m)
+	return cmd.Run()
+}
+
+func createTmpScript(command string) (string, error) {
+	scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.bat", os.TempDir(), util.RandomString(10))
+	if err := os.WriteFile(scriptFile, []byte(command), 0700); err != nil {
+		return "", err
+	}
+	return scriptFile, nil
+}
+
+func defaultConfigFile() string {
+	homeDir, _ := os.UserConfigDir()
+	return filepath.Join(homeDir, defaultClientUserConfigFileRelative)
+}
diff --git a/docs/publish.md b/docs/publish.md
index 69d162d0702e8a1d105063c4c41a9e260ba2a024..e6ffddec84828ca6af189740120f8035733f7d2d 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -38,7 +38,7 @@ Here's an example showing how to publish a simple message using a POST request:
 
 === "PowerShell"
     ``` powershell
-    Invoke-RestMethod -Method 'Post' -Uri https://ntfy.sh/topic -Body "Backup successful 😀" -UseBasicParsing
+    Invoke-RestMethod -Method 'Post' -Uri https://ntfy.sh/mytopic -Body "Backup successful" -UseBasicParsing
     ```
 
 === "Python"
diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md
index 52e005c0badb52ab6e523ced2c717be1610c3387..8546816686b79f3e81aa82cc91cde426558cd3e4 100644
--- a/docs/subscribe/cli.md
+++ b/docs/subscribe/cli.md
@@ -145,12 +145,27 @@ Here's an example config file that subscribes to three different topics, executi
             fi
     ```
 
+    === "%AppData%\ntfy\client.yml"
+    ```
+    subscribe:
+    - topic: echo-this
+      command: 'echo Message received: %message%'
+    - topic: calc
+      command: calc
+      if:
+        priority: high,urgent
+    - topic: toastthis
+      command: |
+        notifu /p "a title: %NTFY_TITLE%" /m "%NTFY_MESSAGE%"
+        exit 0
+    ```
+
 In this example, when `ntfy subscribe --from-config` is executed:
 
 * Messages to `echo-this` simply echos to standard out
 * Messages to `alerts` display as desktop notification for high priority messages using [notify-send](https://manpages.ubuntu.com/manpages/focal/man1/notify-send.1.html)
 * Messages to `calc` open the gnome calculator 😀 (*because, why not*)
-* Messages to `print-temp` execute an inline script and print the CPU temperature
+* Messages to `print-temp` execute an inline script and print the CPU temperature (Linux version only)
 
 I hope this shows how powerful this command is. Here's a short video that demonstrates the above example:
 
diff --git a/util/util.go b/util/util.go
index 3919d3e2384c8772c2573344a540a2ce4441acdc..7fc22d091bdb714392c307b9481f103a82969d22 100644
--- a/util/util.go
+++ b/util/util.go
@@ -183,11 +183,6 @@ func PriorityString(priority int) (string, error) {
 	}
 }
 
-// ExpandHome replaces "~" with the user's home directory
-func ExpandHome(path string) string {
-	return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME"))
-}
-
 // ShortTopicURL shortens the topic URL to be human-friendly, removing the http:// or https://
 func ShortTopicURL(s string) string {
 	return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
diff --git a/util/util_test.go b/util/util_test.go
index a3cf4a6cf3ddc544e6990f37303a4ab001d97590..508e96bf976d418b506c887cc1a2aabdb5fb8bd2 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -3,7 +3,6 @@ package util
 import (
 	"github.com/stretchr/testify/require"
 	"io/ioutil"
-	"os"
 	"path/filepath"
 	"testing"
 	"time"
@@ -75,14 +74,6 @@ func TestSplitNoEmpty(t *testing.T) {
 	require.Equal(t, []string{"tag1", "tag2"}, SplitNoEmpty("tag1,tag2,", ","))
 }
 
-func TestExpandHome_WithTilde(t *testing.T) {
-	require.Equal(t, os.Getenv("HOME")+"/this/is/a/path", ExpandHome("~/this/is/a/path"))
-}
-
-func TestExpandHome_NoTilde(t *testing.T) {
-	require.Equal(t, "/this/is/an/absolute/path", ExpandHome("/this/is/an/absolute/path"))
-}
-
 func TestParsePriority(t *testing.T) {
 	priorities := []string{"", "1", "2", "3", "4", "5", "min", "LOW", "   default ", "HIgh", "max", "urgent"}
 	expected := []int{0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 5}