diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## [2.2.0](https://github.com/haskell-github-trust/effectful-cli/compare/2.1.0...2.2.0) (2026-10-27)
+
+### Features
+
+* add reversible cli switches ([28c1b75](https://github.com/haskell-github-trust/effectful-cli/commit/28c1b75708fafd8f497d2bc0912ff7be4760a8d0))
+
 ## [2.1.0](https://github.com/haskell-github-trust/effectful-cli/compare/2.0.0...2.1.0) (2026-10-26)
 
 ### Features
diff --git a/effectful-cli.cabal b/effectful-cli.cabal
--- a/effectful-cli.cabal
+++ b/effectful-cli.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           effectful-cli
-version:        0.2.1.0
+version:        0.2.2.0
 synopsis:       CLI helpers for the effectful ecosystem
 description:    Please see the README at https://github.com/haskell-github-trust/effectful-cli
 category:       CLI
diff --git a/src/Data/CLI/Impl.hs b/src/Data/CLI/Impl.hs
--- a/src/Data/CLI/Impl.hs
+++ b/src/Data/CLI/Impl.hs
@@ -6,6 +6,7 @@
   noArgs,
   forceSwitch,
   noopSwitch,
+  reversibleSwitch,
   uuidArg,
   mkCommand,
   mkCommands,
@@ -69,6 +70,15 @@
 -- @since 0.1.0.0
 noopSwitch :: Parser Bool
 noopSwitch = switch (short 'n' <> long "noop" <> help "do not perform any real action")
+
+-- | @--name@/@--no-name@ switch pair; the last occurrence wins, otherwise the default.
+--
+-- @since 0.2.2.0
+reversibleSwitch :: Bool -> String -> String -> Parser Bool
+reversibleSwitch def name h =
+  last
+    <$> some (flag' True (long name <> help h) <|> flag' False (long ("no-" <> name) <> hidden))
+      <|> pure def
 
 -- | UUID given as a positional argument or @-u@/@--uuid@.
 --
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE OverloadedStrings #-}
 
-import Data.CLI.Impl (fromParsec)
+import Data.CLI.Impl (fromParsec, reversibleSwitch)
 import Data.Error.CLI (CLIError (..), displayCLIError)
 import Effectful (Eff, IOE, liftIO, runEff)
 import Effectful.CLI.Conc (batchConcurrentlyWithIndex)
@@ -78,3 +78,10 @@
 
     it "rejects an invalid input" $
       parse ["ko"] `shouldBe` Nothing
+
+  describe "reversibleSwitch"
+    $ it "uses the default or the last occurrence"
+    $ let p = info (reversibleSwitch True "labels" "show labels") mempty
+          parse = getParseResult . execParserPure defaultPrefs p
+       in fmap parse [[], ["--no-labels"], ["--no-labels", "--labels"], ["--labels", "--no-labels"]]
+            `shouldBe` fmap Just [True, False, True, False]
