diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Changelog
 
+## 0.4 (2022-03-27)
+- template.cabal: use cabal 2.0 and define hs-source-dirs
+- 'tagdist --existing-tag' replaces 'dist' command
+- 'upload': add --existing-tag for dist rather than tagdist
+- 'new': stack init with lts-17
+- 'rename': experimental command to rename a project
+- add '--no-hlint' option to skip running hlint
+  (also hlint is no longer run for 'publish')
+
 ## 0.3 (2020-05-07)
 - only read Hackage username/password if not in ~/.cabal/config
 - 'upload': show newer untagged commits
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 `hkgr` (pronounced "hackager") is a tool for making releases of
 Haskell packages on Hackage.
 
-It uses a cautious stepped approach to releases.
+It uses a cautious stepped iterative approach to releases.
 
 ## Example usage
 
@@ -46,8 +46,8 @@
 Source tarball created: dist/hkgr-0.2.5.tar.gz
 ```
 
-Alternatively if you have manually tagged the version
-with `vX.Y.Z` you can use `hkgr dist` to create a tarball.
+Alternatively if you had manually tagged the release with `v0.2.5`
+you can use `hkgr tagdist --existing-tag` to create a dist tarball.
 
 The tarball can now be uploaded to Hackage as a candidate release:
 ```
@@ -57,9 +57,14 @@
 ```
 
 One can continue to `tagdist -f` and `upload` until
-everything looks good and CI passed etc,
-then it is time to push the final tag and publish the release:
+everything looks good and CI passed etc.
 
+Note that `tagdist -f` and `upload`, can be combined as `upload -f`.
+If one wants to check the candidate upload quickly,
+it is faster just to run `upload` followed by `upload -f` if needed.
+
+Then it is time to push the final tag and publish the release:
+
 ```
 $ hkgr publish
 Everything up-to-date
@@ -96,16 +101,19 @@
 
 If sdist fails for some reason then hkgr tries to reset the tag.
 
-### dist
 Alternatively if you have already manually tagged a release with 'v' prefix
-you can use `hkgr dist` to create a tarball.
+you can use `--existing-tag` to create a dist tarball.
 
 ### upload
 `hkgr upload` uploads the tarball to Hackage as a candidate release.
-Like `hkgr tagdist -f`, this can be repeated.
+Like `hkgr tagdist -f`, `hkgr upload -f` can be repeated.
 
-Haddock draft documentation can also be uploaded if desired with `hkgr upload-haddock`.
+Haddock draft documentation can also be uploaded once if desired
+with `hkgr upload-haddock`.
 
+If you have an existing version tag (starting with `v`) you can use
+the `--existing-tag` option to skip the tagging step (like for `tagdist`).
+
 ### publish
 `hkgr publish` releases the tarball to Hackage.
 
@@ -127,8 +135,7 @@
 
 A `stack.yaml` file and git repo is also set up.
 
-One can use `hub create` etc to create the project on Github.
+One can use `gh repo create` etc to create the project repo on Github.
 
 ## Requirements
-
 hkgr uses `cabal-install` >=2, `git`, and also `hlint` if available.
diff --git a/data/template.cabal.tmpl b/data/template.cabal.tmpl
--- a/data/template.cabal.tmpl
+++ b/data/template.cabal.tmpl
@@ -14,17 +14,20 @@
 build-type:          Simple
 --  extra-doc-files:     README.md
 --                       ChangeLog.md
-cabal-version:       1.18
---tested-with:         GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.4
+cabal-version:       2.0
+-- tested-with:         GHC == 8.6.5
+--                        | == 8.8.4
+--                        | == 8.10.7
+--                        | == 9.0.2
 
 source-repository head
   type:                git
   location:            https://github.com/@USER@/@PROJECT@.git
 
 executable @PROJECT@
-  main-is:             src/Main.hs
+  main-is:             Main.hs
 --  other-modules:       Paths_@PROJECT_@
---  hs-source-dirs:      app
+  hs-source-dirs:      src
   build-depends:       base < 5,
                        @PROJECT@
   default-language:    Haskell2010
diff --git a/hkgr.cabal b/hkgr.cabal
--- a/hkgr.cabal
+++ b/hkgr.cabal
@@ -1,9 +1,9 @@
 cabal-version:       2.0
 name:                hkgr
-version:             0.3
+version:             0.4
 synopsis:            Simple Hackage release workflow for package maintainers
 description:
-            Hkgr (pronounced "Hackager") is a tool to help make new releases of
+            Hkgr (Hackager) is a tool to help make new releases of
             Haskell packages, with commands for git tagging, pristine sdist,
             and uploading to Hackage.
             There is also a command for creating a new local project.
@@ -11,17 +11,17 @@
 bug-reports:         https://github.com/juhp/hkgr/issues
 license:             GPL-3
 license-file:        LICENSE
-author:              Jens Petersen
-maintainer:          juhpetersen@gmail.com
-copyright:           2019-2021  Jens Petersen
+author:              Jens Petersen <juhpetersen@gmail.com>
+maintainer:          Jens Petersen <juhpetersen@gmail.com>
+copyright:           2019-2022  Jens Petersen
 category:            Util
 build-type:          Simple
 data-dir:            data
 data-files:          template.cabal.tmpl
 extra-doc-files:     README.md
                    , CHANGELOG.md
-tested-with:         GHC == 8.8.4, GHC == 8.6.5, GHC == 8.4.4, GHC == 8.2.2,
-                     GHC == 8.0.2
+tested-with:         GHC == 8.10.5, GHC == 8.8.4, GHC == 8.6.5, GHC == 8.4.4,
+                     GHC == 8.2.2, GHC == 8.0.2
 
 source-repository head
   type:                git
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -33,25 +33,39 @@
     "'Hackager' is a package release tool for easy Hackage workflow" $
     subcommands
     [ Subcommand "new" "setup a new project" $
-      newCmd <$> optional (strArg "PROJECT")
+      newCmd
+      <$> optional (strArg "PROJECT")
     , Subcommand "tagdist" "'git tag' version and 'cabal sdist' tarball" $
-      tagDistCmd False <$> forceOpt "Move existing tag"
-    , Subcommand "dist" "'cabal sdist' tarball" $
-      tagDistCmd True <$> forceOpt "Recreate tarball"
+      tagDistCmd
+      <$> existingTag
+      <*> forceOpt "Move existing tag"
+      <*> hlintOpt
     , Subcommand "upload" "'cabal upload' candidate tarball to Hackage" $
-      uploadCmd False <$> forceOpt "Move existing tag"
+      uploadCmd False
+      <$> existingTag
+      <*> forceOpt "Move existing tag"
+      <*> hlintOpt
     , Subcommand "publish" "Publish to Hackage ('cabal upload --publish')" $
-      pure $ uploadCmd True False
+      pure $ uploadCmd True False False True
     , Subcommand "upload-haddock" "Upload candidate documentation to Hackage" $
       pure $ upHaddockCmd False
     , Subcommand "publish-haddock" "Publish documentation to Hackage" $
       pure $ upHaddockCmd True
     , Subcommand "version" "Show the package version from .cabal file" $
       pure showVersionCmd
+    , Subcommand "rename" "Rename the Cabal package" $
+      renameCmd
+      <$> strArg "NEWNAME"
     ]
   where
     forceOpt = switchWith 'f' "force"
 
+    existingTag =
+      switchWith 'T' "existing-tag" "Use existing tag to create tarball"
+
+    hlintOpt =
+      switchWith 'H' "no-hlint" "Skip running hlint"
+
 git :: String -> [String] -> P.ProcessConfig () () ()
 git c args = P.proc "git" (c:args)
 
@@ -100,26 +114,44 @@
 error' = error
 #endif
 
-tagDistCmd :: Bool -> Bool -> IO ()
-tagDistCmd sdistOnly force = do
+tarGzExt :: String
+tarGzExt = "tar.gz"
+
+assertTagOnBranch :: String -> IO ()
+assertTagOnBranch tag =
+    whenM (null <$> cmdOut (git "branch" ["--contains", "tags/" ++ tag])) $
+    error' $ tag ++ " is no longer on branch: use --force to move it"
+
+tagDistCmd :: Bool -> Bool -> Bool -> IO ()
+tagDistCmd existingtag force noHlint = do
   pkgid <- checkPackage True
   let tag = pkgidTag pkgid
-  tagHash <- cmdMaybe (git "rev-parse" [tag])
-  if sdistOnly
-    then do
-    when (isNothing tagHash) $
-      error' $ "tag " ++ tag ++ " do not exist"
-    sdist force pkgid
+  mtagHash <- cmdMaybe (git "rev-parse" [tag])
+  when (not force && isJust mtagHash) $ assertTagOnBranch tag
+  if existingtag
+    then if isNothing mtagHash
+         then error' $ "tag " ++ tag ++ " does not exist"
+         else sdist force noHlint pkgid
     else do
-    when (isJust tagHash && not force) $
-      error' $ "tag " ++ tag ++ " exists: use --force to override and move"
-    git_ "tag" $ ["--force" | force] ++ [tag]
-    unless force $ putStrLn tag
-    sdist force pkgid `onException` do
-      putStrLn "Resetting tag"
-      if force
-        then git_ "tag" ["--force", tag, fromJust tagHash]
-        else git_ "tag" ["--delete", tag]
+    if isJust mtagHash && not force
+      then do
+      headHash <- cmdOut (git "rev-parse" ["HEAD"])
+      let onHead = Just headHash == mtagHash
+      putStrLn $ "tag is " ++ (if onHead then "" else "not ") ++ "on HEAD"
+      let tarball = sdistDir </> showPkgId pkgid <.> tarGzExt
+      exists <- doesFileExist tarball
+      if exists
+        then error' $ "tag " ++ tag ++ " exists: use --force to " ++
+             (if exists then "override tarball and " else "") ++ "move"
+        else sdist force noHlint pkgid
+      else do
+      git_ "tag" $ ["--force" | force] ++ [tag]
+      unless force $ putStrLn tag
+      sdist force noHlint pkgid `onException` do
+        putStrLn "Resetting tag"
+        if force
+          then git_ "tag" ["--force", tag, fromJust mtagHash]
+          else git_ "tag" ["--delete", tag]
 
 pkgidTag :: PackageIdentifier -> String
 pkgidTag pkgid = "v" ++ packageVersion pkgid
@@ -149,20 +181,20 @@
 
     checkNotPublished :: PackageIdentifier -> IO ()
     checkNotPublished pkgid = do
-      let published = sdistDir </> showPkgId pkgid <.> ".tar.gz" <.> "published"
+      let published = sdistDir </> showPkgId pkgid <.> tarGzExt <.> "published"
       exists <- doesFileExist published
       when exists $ error' $ showPkgId pkgid <> " was already published!!"
-      let oldpublished = "dist" </> showPkgId pkgid <.> ".tar.gz" <.> "published"
+      let oldpublished = "dist" </> showPkgId pkgid <.> tarGzExt <.> "published"
       oldExists <- doesFileExist oldpublished
       when oldExists $ error' $ showPkgId pkgid <> " was already published!!"
 
 sdistDir :: FilePath
 sdistDir = ".hkgr"
 
-sdist :: Bool -> PackageIdentifier -> IO ()
-sdist force pkgid = do
+sdist :: Bool -> Bool -> PackageIdentifier -> IO ()
+sdist force noHlint pkgid = do
   let tag = pkgidTag pkgid
-  let target = sdistDir </> showPkgId pkgid <.> ".tar.gz"
+  let target = sdistDir </> showPkgId pkgid <.> tarGzExt
   haveTarget <- doesFileExist target
   when haveTarget $
     if force
@@ -175,10 +207,11 @@
     whenM (doesFileExist ".gitmodules") $
       git_ "submodule" ["update", "--init"]
     cabal_ "check" []
-    mhlint <- findExecutable "hlint"
-    when (isJust mhlint) $ do
-      putStrLn "Running hlint"
-      void $ P.runProcess $ P.proc "hlint" ["--no-summary", "."]
+    unless noHlint $ do
+      mhlint <- findExecutable "hlint"
+      when (isJust mhlint) $ do
+        putStrLn "Running hlint"
+        void $ P.runProcess $ P.proc "hlint" ["--no-summary", "."]
     let dest = takeDirectory $ cwd </> target
     unlessM (doesDirectoryExist dest) $
       createDirectoryIfMissing True dest
@@ -190,17 +223,16 @@
   putStrLn $ packageVersion pkgid
 
 -- FIXME cabal install creates tarballs now
-uploadCmd :: Bool -> Bool -> IO ()
-uploadCmd publish force = do
+uploadCmd :: Bool -> Bool -> Bool -> Bool -> IO ()
+uploadCmd publish existingtag force noHlint = do
   needProgram "cabal"
   pkgid <- checkPackage False
-  let file = sdistDir </> showPkgId pkgid <.> ".tar.gz"
+  let tarball = sdistDir </> showPkgId pkgid <.> tarGzExt
       tag = pkgidTag pkgid
-  exists <- doesFileExist file
+  exists <- doesFileExist tarball
   when (force || not exists) $
-    tagDistCmd False force
-  whenM (null <$> cmdOut (git "branch" ["--contains", "tags/" ++ tag])) $
-    error' $ tag ++ " is no longer on branch: use --force to move it"
+    tagDistCmd existingtag force noHlint
+  assertTagOnBranch tag
   untagged <- cmdLines $ git "log" ["--pretty=reference", tag ++ "..HEAD"]
   unless (null untagged) $ do
     putStrLn "untagged newer commits:"
@@ -215,10 +247,10 @@
       putStrLn "done"
     git_ "push" ["origin", tag]
   userpassBS <- getUserPassword
-  void $ P.readProcessInterleaved_ (P.setStdin (P.byteStringInput userpassBS) $ P.proc "cabal" ("upload" : ["--publish" | publish] ++ [file]))
+  void $ P.readProcessInterleaved_ (P.setStdin (P.byteStringInput userpassBS) $ P.proc "cabal" ("upload" : ["--publish" | publish] ++ [tarball]))
   putStrLn $ (if publish then "Published at " else "Uploaded to ") ++ "https://hackage.haskell.org/package/" ++ showPkgId pkgid ++ if publish then "" else "/candidate"
   when publish $
-    createFileLink (takeFileName file) (file <.> "published")
+    createFileLink (takeFileName tarball) (tarball <.> "published")
 
 getUserPassword :: IO B.ByteString
 getUserPassword = do
@@ -298,7 +330,7 @@
         else do
         -- filter out dirs
         dirs <- filterM doesDirectoryExist files
-        if dirs == files then error' "Could not guess name"
+        if dirs == files then error' "Could not guess name: subdirectories found"
           else
           case filter ("cabal" `isExtensionOf`) $ files \\ dirs of
             [] -> takeFileName <$> getCurrentDirectory
@@ -329,7 +361,7 @@
   mstack <- findExecutable "stack"
   -- FIXME add stack.yaml template too
   when (not haveStackCfg && isJust mstack) $ do
-    cmd_ "stack" ["init", "--verbosity", "warn", "--resolver", "lts-16"]
+    cmd_ "stack" ["init", "--verbosity", "warn", "--resolver", "lts-19"]
     sed ["/^#/d", "/^$/d"] "stack.yaml"
   haveGit <- doesDirectoryExist ".git"
   unless haveGit $ do
@@ -344,11 +376,6 @@
         [cbl] -> return $ Just (takeBaseName cbl)
         _ -> error' "More than one .cabal file found!"
 
-    sed :: [String] -> FilePath -> IO ()
-    sed [] _ = error' "sed given no script"
-    sed args file =
-      cmd_ "sed" $ ["-i", "-e"] ++ intersperse "-e" args ++ [file]
-
     setupCabalTemplate :: String -> IO ()
     setupCabalTemplate name = do
       userTemplate <- getUserConfigFile "hkgr" "template.cabal"
@@ -376,21 +403,33 @@
       unlessM (doesFileExist modulePath) $ do
         createDirectoryIfMissing True $ takeDirectory modulePath
         writeFile modulePath "-- SPDX-License-Identifier: BSD-3-Clause\n\nmodule MyLib where\n"
-        -- sed ["-i", "1s/^/-- SPDX-License-Identifier: BSD-3-Clause\n\n/"] modulePath
       where
-        replaceHolder lbl val file =
-          sed ["s/@" ++ lbl ++ "@/" ++ val ++ "/"] file
+        replaceHolder lbl val =
+          sed ["s/@" ++ lbl ++ "@/" ++ val ++ "/"]
 
         underscore '-' = '_'
         underscore c = c
 
+sed :: [String] -> FilePath -> IO ()
+sed [] _ = error' "sed given no script"
+sed args file =
+  cmd_ "sed" $ ["-i", "-e"] ++ intersperse "-e" args ++ [file]
+
 #if !MIN_VERSION_filepath(1,4,2)
-    isExtensionOf :: String -> FilePath -> Bool
-    isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions
-    isExtensionOf ext         = isSuffixOf ('.':ext) . takeExtensions
+isExtensionOf :: String -> FilePath -> Bool
+isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions
+isExtensionOf ext         = isSuffixOf ('.':ext) . takeExtensions
 #endif
 
 #if !MIN_VERSION_extra(1,6,15)
 fromMaybeM :: Monad m => m a -> m (Maybe a) -> m a
 fromMaybeM n = maybeM n return
 #endif
+
+renameCmd :: String -> IO ()
+renameCmd newname = do
+  pkgid <- getPackageId
+  let oldname = unPackageName (pkgName pkgid)
+  sed ["s/" ++ oldname ++ "/" ++ newname ++ "/g"] $ oldname <.> "cabal"
+  renameFile (oldname <.> "cabal") (newname <.> "cabal")
+  -- FIXME adjust README
