diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Sridhar Ratnakumar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# hackage-publish
+
+**Goal**: A simple script to publish Haskell packages to Hackage with little fanfare.
+
+## What it does
diff --git a/hackage-publish.cabal b/hackage-publish.cabal
new file mode 100644
--- /dev/null
+++ b/hackage-publish.cabal
@@ -0,0 +1,69 @@
+cabal-version:      2.4
+name:               hackage-publish
+version:            0.1.0.0
+license:            MIT
+copyright:          2025 Sridhar Ratnakumar
+maintainer:         srid@srid.ca
+author:             Sridhar Ratnakumar
+category:           Web
+synopsis:           No frills releasing to Hackage
+
+-- A longer description of the package.
+-- description:
+
+-- A URL where users can report bugs.
+-- bug-reports:
+
+extra-source-files:
+  LICENSE
+  README.md
+
+common shared
+  ghc-options:
+    -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns
+    -Wmissing-deriving-strategies -Wunused-foralls -Wunused-foralls
+    -fprint-explicit-foralls -fprint-explicit-kinds
+
+  mixins:
+    base hiding (Prelude),
+    relude (Relude as Prelude, Relude.Container.One),
+    relude
+
+  default-extensions:
+    DataKinds
+    DerivingStrategies
+    DerivingVia
+    LambdaCase
+    MultiWayIf
+    NoStarIsType
+    OverloadedStrings
+    StrictData
+    TypeFamilies
+    ViewPatterns
+
+  build-depends:
+    , aeson
+    , async
+    , base              >=4   && <5
+    , data-default
+    , directory
+    , filepath
+    , mtl
+    , optics-core
+    , profunctors
+    , relude            >=1.0
+    , shh
+    , shower
+    , template-haskell
+    , temporary
+    , time
+    , which
+    , with-utf8
+
+  hs-source-dirs:     src
+  default-language:   GHC2021
+
+executable hackage-publish
+  import:        shared
+  main-is:       Main.hs
+  other-modules: Shh.Nix
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE ExtendedDefaultRules #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -Wno-type-defaults #-}
+
+module Main where
+
+import Main.Utf8 qualified as Utf8
+import Shh
+import Shh.Nix (loadExeNix)
+import System.Directory
+import System.FilePath
+import System.IO.Temp
+
+-- Load executables with static Nix paths:
+$(loadExeNix "cabal" "cabal")
+$(loadExeNix "op" "op")
+
+main :: IO ()
+main = do
+  Utf8.withUtf8 $ do
+    withSystemTempDirectory "hackage-publish" $ \tmpDir -> do
+      putTextLn $ "Using temporary directory: " <> show tmpDir
+
+      -- Run cabal sdist
+      putTextLn "Creating source distribution..."
+      cabal "sdist" "-o" tmpDir
+
+      -- Get password from 1password
+      putTextLn "Retrieving password from 1password..."
+      password <- op "read" "op://Private/Hackage/password" |> captureTrim
+
+      -- Find the tarball file
+      files <- listDirectory tmpDir
+      let maybeTarball = viaNonEmpty head $ filter (\f -> takeExtension f == ".gz") files
+      case maybeTarball of
+        Nothing -> error "No .gz tarball found in sdist output"
+        Just tarball -> do
+          -- Upload to hackage
+          putTextLn "Publishing to Hackage..."
+          cabal "upload" "--publish" "-u" "sridca" "-p" (decodeUtf8 @String password) (tmpDir </> tarball)
+
+      putTextLn "Successfully published to Hackage!"
diff --git a/src/Shh/Nix.hs b/src/Shh/Nix.hs
new file mode 100644
--- /dev/null
+++ b/src/Shh/Nix.hs
@@ -0,0 +1,14 @@
+module Shh.Nix (loadExeNix) where
+
+import Language.Haskell.TH
+import Shh.Internal (rawExe)
+import System.Which (staticWhichNix)
+
+-- Template Haskell function that combines staticWhichNix with shh to create
+-- a Cmd function using the executable's static Nix store path at compile time.
+loadExeNix :: String -> String -> Q [Dec]
+loadExeNix fnName exeName = do
+  pathExp <- staticWhichNix exeName
+  case pathExp of
+    LitE (StringL nixStorePath) -> rawExe fnName nixStorePath
+    _ -> fail $ "staticWhichNix didn't return a string literal for " ++ exeName
