diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Changelog for hackage-cli
 
+## 0.2.0.0
+
+_Andreas Abel, 2026-04-18_
+
+- Remove `push-candidate` command
+  (PR [#79](https://github.com/hackage-trustees/hackage-cli/pull/79)).
+
+Builds with `Cabal 3.4 - 3.16` and `GHC 8.2 - 9.14.1`.
+
 ## 0.1.0.4
 
 _Andreas Abel, 2025-08-30_
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -31,7 +31,6 @@
   push-cabal               upload revised .cabal files
   sync-cabal               upadate/sync local .cabal file with latest revision
                            on Hackage
-  push-candidate           upload package candidate(s)
   list-versions            list versions for a package
   check-revision           validate revision
   index-sha256sum          generate sha256sum-format file
diff --git a/hackage-cli.cabal b/hackage-cli.cabal
--- a/hackage-cli.cabal
+++ b/hackage-cli.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                hackage-cli
-version:             0.1.0.4
+version:             0.2.0.0
 
 synopsis:            CLI tool for Hackage
 description:
@@ -19,18 +19,19 @@
 tested-with:
   -- Keep in descending order.
   GHC == 9.14.1
-  GHC == 9.12.2
-  GHC == 9.10.2
+  GHC == 9.12.4
+  GHC == 9.10.3
   GHC == 9.8.4
   GHC == 9.6.7
   GHC == 9.4.8
   GHC == 9.2.8
   GHC == 9.0.2
-  GHC == 8.10.7
-  GHC == 8.8.4
-  GHC == 8.6.5
-  GHC == 8.4.4
-  GHC == 8.2.2
+  -- -- 2026-04-18: Stop testing GHC 8 on CI
+  -- GHC == 8.10.7
+  -- GHC == 8.8.4
+  -- GHC == 8.6.5
+  -- GHC == 8.4.4
+  -- GHC == 8.2.2
 
 extra-doc-files:
   CHANGELOG.md
@@ -59,7 +60,7 @@
     , base         >= 4.10.0.0 && < 5
     , bytestring   >= 0.10.4.0 && < 0.13
     , Cabal        >= 3.4      && < 3.17
-    , containers   >= 0.5.0.0  && < 0.8
+    , containers   >= 0.5.0.0  && < 1
     , mtl          >= 2.2.2    && < 2.3   || >= 2.3.1 && < 2.4
     , pretty      ^>= 1.1.2
 
@@ -134,7 +135,7 @@
     , tagsoup               ^>= 0.14
     , tar                    >= 0.5      && < 1
     , text                   >= 1.2      && < 2.2
-    , time                   >= 1.5.0.1  && < 1.15
+    , time                   >= 1.5.0.1  && < 2
     , zlib                   >= 0.6.1    && < 0.8
 
   ghc-options: -Wall -Wcompat -threaded
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -11,7 +11,7 @@
 -- Copyright   :  Herbert Valerio Riedel, Andreas Abel
 -- SPDX-License-Identifier: GPL-3.0-or-later
 --
-module Main where
+module Main (main) where
 
 import           Prelude                                hiding (log)
 
@@ -67,7 +67,6 @@
 import           System.Directory
 import           System.Environment                     (lookupEnv)
 import           System.Exit                            (ExitCode (..), exitFailure)
-import           System.FilePath
 import           System.IO                              (hPutStrLn, stderr)
 import           System.IO.Error                        (tryIOError, isDoesNotExistError)
 import qualified System.IO.Streams                      as Streams
@@ -137,34 +136,6 @@
     liftIO $ sendRequest c q1 emptyBody
     hcReqCnt += 1
 
-hackagePutTgz :: ByteString -> ByteString -> HIO ByteString
-hackagePutTgz p tgz = do
-    q1 <- liftIO $ buildRequest $ do
-        http PUT p
-        setUA
-        -- setAccept "application/json" -- wishful thinking
-        setContentType "application/x-tar"
-        -- setContentEncoding "gzip"
-        setContentLength (fromIntegral $ BS.length tgz)
-
-    lft <- use hcReqLeft
-    unless (lft > 0) $
-        fail "hackagePutTgz: request budget exhausted for current connection"
-
-    c <- openHConn
-    liftIO $ sendRequest c q1 (bsBody tgz)
-    resp <- liftIO $ try (receiveResponse c concatHandler')
-    closeHConn
-    hcReqCnt += 1
-
-    case resp of
-        Right bs -> -- do
-            -- liftIO $ BS.writeFile "raw.out" bs
-            return bs
-
-        Left e@HttpClientError {} -> -- do
-            return (BS8.pack $ show e)
-
 hackageRecvResp :: HIO ByteString
 hackageRecvResp = do
     c <- openHConn
@@ -256,47 +227,6 @@
 bsBody :: ToBuilder a => a -> Streams.OutputStream Builder.Builder -> IO ()
 bsBody bs = Streams.write (Just (toBuilder bs))
 
--- | Upload a candidate to Hackage
---
--- This is a bit overkill, as one could easily just use @curl(1)@ for this:
---
--- > curl --form package=@"$PKGID".tar.gz -u "${CREDS}" https://hackage.haskell.org/packages/candidates/
---
-hackagePushCandidate :: (ByteString,ByteString) -> (FilePath,ByteString) -> HIO ByteString
-hackagePushCandidate cred (tarname,rawtarball) = do
-    when (boundary `BS.isInfixOf` rawtarball) $ fail "WTF... tarball contains boundary-pattern"
-
-    q1 <- liftIO $ buildRequest $ do
-        http POST urlpath
-        setUA
-        uncurry setAuthorizationBasic cred
-        setAccept "application/json" -- wishful thinking
-        setContentType ("multipart/form-data; boundary="<>boundary) -- RFC2388
-        setContentLength bodyLen
-
-    c <- reOpenHConn
-
-    liftIO $ sendRequest c q1 (bsBody body)
-
-    resp <- liftIO $ try (receiveResponse c (\r is -> (,) r <$> concatHandler r is))
-    closeHConn
-
-    case resp of
-        Right (rc,bs) -> do
-            return (BS8.pack (show rc) <> bs)
-        Left (HttpClientError code bs) -> return (BS8.pack ("code=" <> show code <> "\n") <> bs)
-            -- Hackage currently timeouts w/ 503 guru meditation errors,
-            -- which usually means that the transaction has succeeded
-  where
-    urlpath = "/packages/candidates/"
-
-    body = Builder.toLazyByteString $
-           multiPartBuilder boundary [ ("package", [("filename", BS8.pack tarname)]
-                                     , ["Content-Type: application/gzip"], rawtarball)]
-    bodyLen = fromIntegral $ BSL.length body
-
-    boundary = "4d5bb1565a084d78868ff0178bdf4f61"
-
 -- | Simplified RFC2388 multipart/form-data formatter
 --
 -- TODO: make a streaming-variant
@@ -501,10 +431,6 @@
   , optPsCFiles   :: [FilePath]
   } deriving Show
 
-data PushPCOptions = PushPCOptions
-  { optPPCFiles :: [FilePath]
-  } deriving Show
-
 data CheckROptions = CheckROptions
   { optCRNew  :: FilePath
   , optCROrig :: FilePath
@@ -524,7 +450,6 @@
     | PullCabal !PullCOptions
     | PushCabal !PushCOptions
     | SyncCabal !SyncCOptions
-    | PushCandidate !PushPCOptions
     | CheckRevision !CheckROptions
     | IndexShaSum   !IndexShaSumOptions
     | AddBound !AddBoundOptions
@@ -576,8 +501,6 @@
                              <*> switch (long "publish"  <> help "publish revision (review-mode)")
                              <*> some (OA.argument str (metavar "CABALFILES..." <> action "file")))
 
-    pushpcoParser = PushCandidate <$> (PushPCOptions <$> some (OA.argument str (metavar "TARBALLS..." <> action "file")))
-
     checkrevParsser = CheckRevision <$> (CheckROptions <$> OA.argument str (metavar "NEWCABAL" <> action "file")
                                                        <*> OA.argument str (metavar "OLDCABAL" <> action "file"))
 
@@ -602,8 +525,6 @@
                                                    (progDesc "Upload revised .cabal files."))
                                          , command "sync-cabal" (info (helper <*> synccoParser)
                                                    (progDesc "Update/sync local .cabal file with latest revision on Hackage."))
-                                         , command "push-candidate" (info (helper <*> pushpcoParser)
-                                                   (progDesc "Upload package candidate(s)."))
                                          , command "list-versions" (info (helper <*> listcoParser)
                                                    (progDesc "List versions for a package."))
                                          , command "check-revision" (info (helper <*> checkrevParsser)
@@ -772,22 +693,6 @@
                putStrLn (replicate 80 '=')
                BS8.putStrLn (tidyHtml tmp)
                putStrLn (replicate 80 '=')
-
-       PushCandidate (PushPCOptions{ optPPCFiles }) -> do
-           (username,password) <- maybe (fail "missing Hackage credentials") return =<< getHackageCreds
-           putStrLn $ "Using Hackage credentials for username " ++ show username
-
-           forM_ optPPCFiles $ \fn -> do
-               putStrLn $ "reading " ++ show fn ++ " ..."
-               rawtar <- BS.readFile fn
-               putStrLn $ "uplading to Hackage..."
-               tmp <- runHConn (hackagePushCandidate (username,password) (takeFileName fn, rawtar))
-
-               putStrLn "Hackage response was:"
-               putStrLn (replicate 80 '=')
-               BS8.putStrLn tmp
-               putStrLn (replicate 80 '=')
-
 
        CheckRevision (CheckROptions{ optCRNew, optCROrig }) -> do
            old <- BS.readFile optCROrig
diff --git a/stack-9.10.yaml b/stack-9.10.yaml
--- a/stack-9.10.yaml
+++ b/stack-9.10.yaml
@@ -1,4 +1,4 @@
-resolver: lts-24.7
+resolver: lts-24.37
 compiler: ghc-9.10.2
 compiler-check: match-exact
 
diff --git a/stack-9.12.yaml b/stack-9.12.yaml
--- a/stack-9.12.yaml
+++ b/stack-9.12.yaml
@@ -1,4 +1,4 @@
-resolver: nightly-2025-08-30
+resolver: nightly-2026-04-15
 compiler: ghc-9.12.2
 compiler-check: match-exact
 
