diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@
   let config : List Callback = [ downloadPage ]
   in config
   ```
+
 #### `imm-wallabag`
 `imm-wallabag` will save webpages into a [Wallabag][wallabag] server through its REST API:
   ```dhall
@@ -138,6 +139,26 @@
   in config
   ```
 
+#### `imm-shiori`
+`imm-shiori` will save webpages into [Shiori][shiori] bookmark manager:
+  ```dhall
+  let Callback : Type =
+    { _executable : Text
+    , _arguments : List Text
+    }
+
+  let wallabag =
+    { _executable = "imm-shiori"
+    , _arguments =
+        [ "--tags"
+        , "TAG1,TAG2,TAG3"
+        ]
+    }
+
+  let config : List Callback = [ shiori ]
+  in config
+  ```
+
 ## Example usage
 
 - Subscribe to a feed through direct URL:
@@ -180,3 +201,4 @@
 [3]: https://dhall-lang.org/
 [flakes]: https://nixos.wiki/wiki/Flakes
 [wallabag]: https://www.wallabag.it/
+[shiori]: https://github.com/go-shiori/shiori
diff --git a/data/callbacks.dhall b/data/callbacks.dhall
--- a/data/callbacks.dhall
+++ b/data/callbacks.dhall
@@ -43,5 +43,14 @@
         ]
       }
 
+-- Check out `imm-shiori --help`
+let shiori =
+      { _executable = "imm-shiori"
+      , _arguments =
+        [ "--tags"
+        , "TAG1,TAG2,TAG3"
+        ]
+      }
+
 let config : List Callback = [ downloadPage ]
 in config
diff --git a/imm.cabal b/imm.cabal
--- a/imm.cabal
+++ b/imm.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                imm
-version:             2.1.2.0
+version:             2.1.3.0
 synopsis:            Execute arbitrary actions for each item from RSS/Atom feeds
 description:         Cf README file
 homepage:            https://github.com/k0ral/imm
@@ -118,4 +118,11 @@
   build-depends: imm, aeson, bytestring, dhall >= 1.27, optparse-applicative, prettyprinter >=1.7.0, refined, req, safe-exceptions, text, time, uri-bytestring
   main-is: Main.hs
   hs-source-dirs: src/wallabag
+  ghc-options: -Wall -fno-warn-unused-do-bind -threaded
+
+executable imm-shiori
+  import: common
+  build-depends: imm, aeson, bytestring, dhall >= 1.27, optparse-applicative, prettyprinter >=1.7.0, refined, safe-exceptions, text, time, typed-process, uri-bytestring
+  main-is: Main.hs
+  hs-source-dirs: src/shiori
   ghc-options: -Wall -fno-warn-unused-do-bind -threaded
diff --git a/src/shiori/Main.hs b/src/shiori/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/shiori/Main.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE UnicodeSyntax #-}
+
+-- Save page related to input RSS/Atom item, into shiori bookmark manager.
+--
+--  Meant to be used as a callback for imm.
+--  {{{ Imports
+
+import Data.Aeson (eitherDecode)
+import Data.ByteString.Lazy (getContents)
+import Imm.Callback
+import Imm.Feed
+import Imm.Link
+import Imm.Pretty
+import Options.Applicative (Parser, execParser, forwardOptions, help, helper, info, long, progDesc, short, strOption, switch)
+import System.Process.Typed
+import URI.ByteString.Extended
+
+-- }}}
+
+data CliOptions = CliOptions
+  { _tags ∷ String
+  , _dryRun ∷ Bool
+  }
+  deriving (Eq, Ord, Read, Show)
+
+parseOptions ∷ MonadIO m ⇒ m CliOptions
+parseOptions = io $ execParser $ info (cliOptions <**> helper) $ progDesc description <> forwardOptions
+ where
+  description = "Save page into shiori bookmark manager, for each new RSS/Atom item."
+
+cliOptions ∷ Parser CliOptions
+cliOptions =
+  CliOptions
+    <$> strOption (long "tags" <> short 't' <> help "Comma-separated tags for this bookmark")
+    <*> switch (long "dry-run" <> help "Disable all I/Os, except for logs.")
+
+main ∷ IO ()
+main = do
+  CliOptions tags dryRun ← parseOptions
+  input ← getContents <&> eitherDecode
+
+  case input ∷ Either String CallbackMessage of
+    Right (CallbackMessage _feedLocation _feedDefinition item) → do
+      unless dryRun $ do
+        case getMainLink item of
+          Just link → saveShiori tags (_linkURI link) >>= exitWith
+          _ → putStrLn ("No main link in item " <> show (prettyName item)) >> exitFailure
+    Left e → putStrLn ("Invalid input: " <> e) >> exitFailure
+  return ()
+
+saveShiori ∷ String → AnyURI → IO ExitCode
+saveShiori tags uri =
+  runProcess $
+    proc "shiori" ["add", show $ pretty uri, "-t", tags]
+      & setStdin nullStream
+      & setStdout inherit
+      & setStderr inherit
diff --git a/src/shiori/Prelude.hs b/src/shiori/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/src/shiori/Prelude.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE UnicodeSyntax #-}
+
+module Prelude (for, io, headFail, headThrow, module Relude, module Relude.Extra.Map) where
+
+import Control.Exception.Safe
+import Relude hiding (Handle, appendFile, force, readFile, stdout, writeFile)
+import Relude.Extra.Map (lookup)
+
+io ∷ MonadIO m ⇒ IO a → m a
+io = liftIO
+
+for ∷ [a] → (a → b) → [b]
+for = flip map
+
+headThrow ∷ MonadThrow m ⇒ Exception e ⇒ e → [a] → m a
+headThrow _ (a : _) = return a
+headThrow e _ = throwM e
+
+headFail ∷ MonadFail m ⇒ String → [a] → m a
+headFail _ (a : _) = return a
+headFail e _ = fail e
