diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Revision history for xdg-desktop-entry
 
+## 0.1.1.7 -- 2026-09-30
+
+* Parse quoted and escaped `Exec` arguments before expanding desktop-entry
+  field codes, preserving argument boundaries and literal percent signs.
+* Quote expanded arguments safely for shell consumers, including embedded
+  apostrophes, dollar signs, and command substitutions (#695).
+
 ## 0.1.1.6 -- 2026-09-04
 
 * Replace the `ini` based parser with one that follows the desktop entry
diff --git a/src/System/Environment/XDG/DesktopEntry.hs b/src/System/Environment/XDG/DesktopEntry.hs
--- a/src/System/Environment/XDG/DesktopEntry.hs
+++ b/src/System/Environment/XDG/DesktopEntry.hs
@@ -17,6 +17,7 @@
 module System.Environment.XDG.DesktopEntry
   ( DesktopEntry (..),
     deCommand,
+    deCommandArgs,
     deComment,
     deHasCategory,
     deIcon,
@@ -138,11 +139,70 @@
   Maybe String
 deComment langs de = deLocalisedAtt langs de "Comment"
 
--- | Return the command that should be executed when running this desktop entry.
+-- | Return a shell-quoted command for launching this entry without files.
 deCommand :: DesktopEntry -> Maybe String
-deCommand de =
-  reverse . dropWhile (== ' ') . reverse . takeWhile (/= '%')
-    <$> lookup "Exec" (deAttributes de)
+deCommand = fmap (unwords . map shellQuote) . deCommandArgs
+
+-- | Parse and expand Exec arguments without invoking a shell. File and URL
+-- placeholders are omitted because no files are being opened.
+deCommandArgs :: DesktopEntry -> Maybe [String]
+deCommandArgs de = do
+  command <- lookup "Exec" (deAttributes de)
+  args <- splitExecArgs $ unescapeExecValue command
+  expanded <- concat <$> traverse expandArg args
+  case expanded of
+    program : _ | not (null program) && '=' `notElem` program -> Just expanded
+    _ -> Nothing
+  where
+    expandArg "%i" = pure $ maybe [] (\icon -> if null icon then [] else ["--icon", icon]) (deIcon de)
+    expandArg "%F" = Just []
+    expandArg "%U" = Just []
+    expandArg arg = do
+      result <- expandCodes arg
+      pure [result | not (null result) || null arg]
+
+    expandCodes [] = Just []
+    expandCodes ('%' : '%' : rest) = ('%' :) <$> expandCodes rest
+    expandCodes ('%' : code : rest)
+      | code `elem` "fudDnNvm" = expandCodes rest
+      | code == 'c' = (deName [] de ++) <$> expandCodes rest
+      | code == 'k' = (deFilename de ++) <$> expandCodes rest
+      | otherwise = Nothing
+    expandCodes ['%'] = Nothing
+    expandCodes (c : rest) = (c :) <$> expandCodes rest
+
+unescapeExecValue :: String -> String
+unescapeExecValue ('\\' : c : rest) =
+  case lookup c [('s', ' '), ('n', '\n'), ('t', '\t'), ('r', '\r'), ('\\', '\\')] of
+    Just decoded -> decoded : unescapeExecValue rest
+    Nothing -> '\\' : c : unescapeExecValue rest
+unescapeExecValue (c : rest) = c : unescapeExecValue rest
+unescapeExecValue [] = []
+
+splitExecArgs :: String -> Maybe [String]
+splitExecArgs = go False False [] []
+  where
+    go quoted started current args []
+      | quoted = Nothing
+      | otherwise = Just $ reverse $ if started then reverse current : args else args
+    go quoted _ current args ('"' : rest) = go (not quoted) True current args rest
+    go _ _ _ _ ['\\'] = Nothing
+    go quoted _ current args ('\\' : c : rest)
+      | quoted && c `notElem` "\"`$\\" = go quoted True (c : '\\' : current) args rest
+      | otherwise = go quoted True (c : current) args rest
+    go quoted started current args (c : rest)
+      | isSpace c && not quoted =
+          go False False [] (if started then reverse current : args else args) rest
+      | otherwise = go quoted True (c : current) args rest
+
+shellQuote :: String -> String
+shellQuote arg
+  | not (null arg) && all safeChar arg = arg
+  | otherwise = "'" ++ concatMap escape arg ++ "'"
+  where
+    safeChar c = isAscii c && (isAlphaNum c || c `elem` "_./:@%+=,-")
+    escape '\'' = "'\\''"
+    escape c = [c]
 
 -- | Return a list of all desktop entries in the given directory.
 listDesktopEntries ::
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -3,6 +3,7 @@
 import System.Environment.XDG.DesktopEntry
 import System.FilePath ((</>))
 import System.IO.Temp (withSystemTempDirectory)
+import System.Process (readProcess)
 import Test.Hspec
 
 fileContent :: [String]
@@ -43,6 +44,33 @@
     print i
     writeFile (filepath i) content
   hspec $ do
+    describe "Exec arguments" $ do
+      let entry command =
+            DesktopEntry
+              (read "Application")
+              "/tmp/Review App.desktop"
+              [("Exec", command), ("Name", "Review App"), ("Icon", "review-icon")]
+      it "preserves arguments following file placeholders" $
+        deCommandArgs (entry "viewer %U --incognito") `shouldBe` Just ["viewer", "--incognito"]
+      it "preserves escaped percentages and later flags" $
+        deCommandArgs (entry "viewer --title=100%% --new-window")
+          `shouldBe` Just ["viewer", "--title=100%", "--new-window"]
+      it "expands metadata without splitting arguments" $
+        deCommandArgs (entry "viewer %c %k %i")
+          `shouldBe` Just ["viewer", "Review App", "/tmp/Review App.desktop", "--icon", "review-icon"]
+      it "removes absent icons and deprecated placeholders" $
+        deCommandArgs (DesktopEntry (read "Application") "app.desktop" [("Exec", "viewer %i %d --flag")])
+          `shouldBe` Just ["viewer", "--flag"]
+      it "preserves quoted and empty arguments" $
+        deCommandArgs (entry "viewer \"two words\" \"\"") `shouldBe` Just ["viewer", "two words", ""]
+      it "rejects unknown field codes, unterminated quotes, and empty commands" $
+        map (deCommandArgs . entry) ["viewer %z", "viewer \"unterminated", "", "%U"]
+          `shouldBe` replicate 4 Nothing
+      it "does not treat desktop arguments as shell expressions" $ do
+        let command = deCommand $ entry "printf %%s \"$(printf injected);'\""
+        case command of
+          Nothing -> expectationFailure "Expected a valid command"
+          Just value -> readProcess "sh" ["-c", value] "" `shouldReturn` "$(printf injected);'"
     describe "deAtt" $ do
       it "content0 should work" $ do
         deResultE <- readDesktopEntry $ filepath 0
diff --git a/xdg-desktop-entry.cabal b/xdg-desktop-entry.cabal
--- a/xdg-desktop-entry.cabal
+++ b/xdg-desktop-entry.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                xdg-desktop-entry
-version:             0.1.1.6
+version:             0.1.1.7
 synopsis:            Parse files conforming to the xdg desktop entry spec
 description:         Parse files conforming to the xdg desktop entry spec.
 bug-reports:         https://github.com/taffybar/taffybar/issues
@@ -43,6 +43,7 @@
                     base
                   , filepath
                   , hspec
+                  , process
                   , temporary
                   , unix
                   , xdg-desktop-entry
