diff --git a/Annex/Drop.hs b/Annex/Drop.hs
--- a/Annex/Drop.hs
+++ b/Annex/Drop.hs
@@ -70,10 +70,10 @@
 	 - will surely fail.
 	 -}
 	checkcopies (have, numcopies, mincopies, _untrusted) Nothing =
-		NumCopies have > numcopies && MinCopies have > mincopies
+		have > fromNumCopies numcopies && have > fromMinCopies mincopies
 	checkcopies (have, numcopies, mincopies, untrusted) (Just u)
-		| S.member u untrusted = NumCopies have >= numcopies && MinCopies have >= mincopies
-		| otherwise = NumCopies have > numcopies && MinCopies have > mincopies
+		| S.member u untrusted = have >= fromNumCopies numcopies && have >= fromMinCopies mincopies
+		| otherwise = have > fromNumCopies numcopies && have > fromMinCopies mincopies
 	
 	decrcopies (have, numcopies, mincopies, untrusted) Nothing =
 		(have - 1, numcopies, mincopies, untrusted)
diff --git a/Annex/NumCopies.hs b/Annex/NumCopies.hs
--- a/Annex/NumCopies.hs
+++ b/Annex/NumCopies.hs
@@ -43,10 +43,10 @@
 import Data.Typeable
 
 defaultNumCopies :: NumCopies
-defaultNumCopies = NumCopies 1
+defaultNumCopies = configuredNumCopies 1
 
 defaultMinCopies :: MinCopies
-defaultMinCopies = MinCopies 1
+defaultMinCopies = configuredMinCopies 1
 
 fromSourcesOr :: v -> [Annex (Maybe v)] -> Annex v
 fromSourcesOr v = fromMaybe v <$$> getM id
@@ -178,8 +178,8 @@
 getNumMinCopiesAttr file =
 	checkAttrs ["annex.numcopies", "annex.mincopies"] file >>= \case
 		(n:m:[]) -> return
-			( NumCopies <$> readish n
-			, MinCopies <$> readish m
+			( configuredNumCopies <$> readish n
+			, configuredMinCopies <$> readish m
 			)
 		_ -> error "internal"
 
@@ -197,8 +197,8 @@
 
 numCopiesCheck' :: RawFilePath -> (Int -> Int -> v) -> [UUID] -> Annex v
 numCopiesCheck' file vs have = do
-	NumCopies needed <- fst <$> getFileNumMinCopies file
-	return $ length have `vs` needed
+	needed <- fst <$> getFileNumMinCopies file
+	return $ length have `vs` fromNumCopies needed
 
 data UnVerifiedCopy = UnVerifiedRemote Remote | UnVerifiedHere
 	deriving (Ord, Eq)
diff --git a/Assistant/Threads/Merger.hs b/Assistant/Threads/Merger.hs
--- a/Assistant/Threads/Merger.hs
+++ b/Assistant/Threads/Merger.hs
@@ -1,6 +1,6 @@
 {- git-annex assistant git merge thread
  -
- - Copyright 2012-2017 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2021 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -17,6 +17,7 @@
 import Utility.DirWatcher.Types
 import Utility.Directory.Create
 import Annex.CurrentBranch
+import Assistant.Commits
 import qualified Annex
 import qualified Annex.Branch
 import qualified Git
@@ -83,7 +84,7 @@
 	mergecurrent =
 		mergecurrent' =<< liftAnnex getCurrentBranch
 	mergecurrent' currbranch@(Just b, _)
-		| changedbranch `isRelatedTo` b =
+		| changedbranch `isRelatedTo` b = do
 			whenM (liftAnnex $ inRepo $ Git.Branch.changed b changedbranch) $ do
 				debug
 					[ "merging", Git.fromRef changedbranch
@@ -99,6 +100,12 @@
 						def
 						cmode
 						changedbranch
+			recordCommit
+		| changedbranch == b =
+			-- Record commit so the pusher pushes it out.
+			-- This makes sure pushes happen when
+			-- annex.autocommit=false
+			recordCommit
 	mergecurrent' _ = noop
 
 {- Is the first branch a synced branch or remote tracking branch related
diff --git a/Assistant/WebApp/Configurators/Preferences.hs b/Assistant/WebApp/Configurators/Preferences.hs
--- a/Assistant/WebApp/Configurators/Preferences.hs
+++ b/Assistant/WebApp/Configurators/Preferences.hs
@@ -85,7 +85,7 @@
 storePrefs :: PrefsForm -> Annex ()
 storePrefs p = do
 	setConfig (annexConfig "diskreserve") (T.unpack $ diskReserve p)
-	setGlobalNumCopies (NumCopies $ numCopies p)
+	setGlobalNumCopies (configuredNumCopies $ numCopies p)
 	unsetConfig (annexConfig "numcopies") -- deprecated
 	setConfig (annexConfig "autoupgrade") (fromAutoUpgrade $ autoUpgrade p)
 	unlessM ((==) <$> pure (autoStart p) <*> inAutoStartFile) $ do
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,24 @@
+git-annex (10.20220504) upstream; urgency=medium
+
+  * Ignore annex.numcopies set to 0 in gitattributes or git config,
+    or by git-annex numcopies or by --numcopies, since that
+    configuration would make git-annex easily lose data. 
+    Same for mincopies.
+  * assistant: When annex.autocommit is set, notice commits that
+    the user makes manually, and push them out to remotes promptly.
+  * multicast: Support uftp 5.0 by switching from aes256-cbc to
+    aes256-gcm.
+  * Fix test failure on NFS when cleaning up gpg temp directory.
+  * Fix a build failure with ghc 9.2.2.
+    Thanks, gnezdo for the patch.
+  * rsync 3.2.4 broke backwards-compatability by preventing exposing
+    filenames to the shell. Made the rsync and gcrypt special remotes
+    detect this and disable shellescape. Closes: #1010397
+  * repair: Avoid treating refs/annex/last-index or other refs that
+    are not commit objects as evidence of repository corruption.
+
+ -- Joey Hess <id@joeyh.name>  Wed, 04 May 2022 11:45:27 -0400
+
 git-annex (10.20220322) upstream; urgency=medium
 
   * Directory special remotes with importtree=yes have changed to once more 
diff --git a/COPYRIGHT b/COPYRIGHT
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -48,7 +48,7 @@
 License: AGPL-3+
 
 Files: Utility/*
-Copyright: 2012-2019 Joey Hess <id@joeyh.name>
+Copyright: 2012-2022 Joey Hess <id@joeyh.name>
 License: BSD-2-clause
 
 Files: doc/logo* */favicon.ico standalone/osx/git-annex.app/Contents/Resources/git-annex.icns
diff --git a/CmdLine/GitAnnex/Options.hs b/CmdLine/GitAnnex/Options.hs
--- a/CmdLine/GitAnnex/Options.hs
+++ b/CmdLine/GitAnnex/Options.hs
@@ -100,8 +100,8 @@
 		)
 	]
   where
-	setnumcopies n = Annex.changeState $ \s -> s { Annex.forcenumcopies = Just $ NumCopies n }
-	setmincopies n = Annex.changeState $ \s -> s { Annex.forcemincopies = Just $ MinCopies n }
+	setnumcopies n = Annex.changeState $ \s -> s { Annex.forcenumcopies = Just $ configuredNumCopies n }
+	setmincopies n = Annex.changeState $ \s -> s { Annex.forcemincopies = Just $ configuredMinCopies n }
 	setuseragent v = Annex.changeState $ \s -> s { Annex.useragent = Just v }
 	setgitconfig v = Annex.addGitConfigOverride v
 	setdesktopnotify v = Annex.changeState $ \s -> s { Annex.desktopnotify = Annex.desktopnotify s <> v }
diff --git a/Command/Drop.hs b/Command/Drop.hs
--- a/Command/Drop.hs
+++ b/Command/Drop.hs
@@ -252,7 +252,7 @@
 			uuid <- getUUID
 			let remoteuuid = fromMaybe uuid $ Remote.uuid <$> mremote
 			locs' <- trustExclude UnTrusted $ filter (/= remoteuuid) locs
-			if NumCopies (length locs') >= numcopies
+			if length locs' >= fromNumCopies numcopies
 				then a numcopies mincopies
 				else stop
 		| otherwise = a numcopies mincopies
diff --git a/Command/Fsck.hs b/Command/Fsck.hs
--- a/Command/Fsck.hs
+++ b/Command/Fsck.hs
@@ -517,8 +517,8 @@
 	locs <- loggedLocations key
 	(untrustedlocations, otherlocations) <- trustPartition UnTrusted locs
 	(deadlocations, safelocations) <- trustPartition DeadTrusted otherlocations
-	let present = NumCopies (length safelocations)
-	if present < numcopies
+	let present = length safelocations
+	if present < fromNumCopies numcopies
 		then ifM (pure (not hasafile) <&&> checkDead key)
 			( do
 				showLongNote $ "This key is dead, skipping."
@@ -527,21 +527,21 @@
 				untrusted <- Remote.prettyPrintUUIDs "untrusted" untrustedlocations
 				dead <- Remote.prettyPrintUUIDs "dead" deadlocations
 				warning $ missingNote desc present numcopies untrusted dead
-				when (fromNumCopies present == 0 && not hasafile) $
+				when (present == 0 && not hasafile) $
 					showLongNote "(Avoid this check by running: git annex dead --key )"
 				return False
 			)
 		else return True
 
-missingNote :: String -> NumCopies -> NumCopies -> String -> String -> String
-missingNote file (NumCopies 0) _ [] dead = 
+missingNote :: String -> Int -> NumCopies -> String -> String -> String
+missingNote file 0 _ [] dead = 
 		"** No known copies exist of " ++ file ++ honorDead dead
-missingNote file (NumCopies 0) _ untrusted dead =
+missingNote file 0 _ untrusted dead =
 		"Only these untrusted locations may have copies of " ++ file ++
 		"\n" ++ untrusted ++
 		"Back it up to trusted locations with git-annex copy." ++ honorDead dead
 missingNote file present needed [] _ =
-		"Only " ++ show (fromNumCopies present) ++ " of " ++ show (fromNumCopies needed) ++ 
+		"Only " ++ show present ++ " of " ++ show (fromNumCopies needed) ++ 
 		" trustworthy copies exist of " ++ file ++
 		"\nBack it up with git-annex copy."
 missingNote file present needed untrusted dead = 
diff --git a/Command/MinCopies.hs b/Command/MinCopies.hs
--- a/Command/MinCopies.hs
+++ b/Command/MinCopies.hs
@@ -32,7 +32,7 @@
 
 startSet :: Int -> CommandStart
 startSet n = startingUsualMessages "mincopies" ai si $ do
-	setGlobalMinCopies $ MinCopies n
+	setGlobalMinCopies $ configuredMinCopies n
 	next $ return True
   where
 	ai = ActionItemOther (Just $ show n)
diff --git a/Command/Multicast.hs b/Command/Multicast.hs
--- a/Command/Multicast.hs
+++ b/Command/Multicast.hs
@@ -152,7 +152,7 @@
 				let ps =
 					-- Force client authentication.
 					[ Param "-c"
-					, Param "-Y", Param "aes256-cbc"
+					, Param "-Y", Param "aes256-gcm"
 					, Param "-h", Param "sha512"
 					-- Picked ecdh_ecdsa for perfect forward secrecy,
 					-- and because a EC key exchange algorithm is
diff --git a/Command/NumCopies.hs b/Command/NumCopies.hs
--- a/Command/NumCopies.hs
+++ b/Command/NumCopies.hs
@@ -8,7 +8,6 @@
 module Command.NumCopies where
 
 import Command
-import qualified Annex
 import Annex.NumCopies
 
 cmd :: Command
@@ -28,11 +27,8 @@
 	Nothing -> giveup $ "Bad number: " ++ s
 	Just n
 		| n > 0 -> startset n
-		| n == 0 -> ifM (Annex.getState Annex.force)
-			( startset n
-			, giveup $ "Setting " ++ setting ++ " to 0 is very unsafe. You will lose data! If you really want to do that, specify --force."
-			)
-		| otherwise -> giveup "Number cannot be negative!"
+		| n == 0 -> giveup $ "Cannot set " ++ setting ++ " to 0."
+		| otherwise -> giveup $ setting ++ " cannot be negative!"
 start' _ _ _ _ = giveup "Specify a single number."
 
 startGet :: CommandStart
@@ -50,7 +46,7 @@
 
 startSet :: Int -> CommandStart
 startSet n = startingUsualMessages "numcopies" ai si $ do
-	setGlobalNumCopies $ NumCopies n
+	setGlobalNumCopies $ configuredNumCopies n
 	next $ return True
   where
 	ai = ActionItemOther (Just $ show n)
diff --git a/Command/Vicfg.hs b/Command/Vicfg.hs
--- a/Command/Vicfg.hs
+++ b/Command/Vicfg.hs
@@ -315,7 +315,7 @@
 			in Right $ cfg { cfgGlobalConfigs = m }
 		| setting == "numcopies" = case readish val of
 			Nothing -> Left "parse error (expected an integer)"
-			Just n -> Right $ cfg { cfgNumCopies = Just (NumCopies n) }
+			Just n -> Right $ cfg { cfgNumCopies = Just (configuredNumCopies n) }
 		| otherwise = badval "setting" setting
 	  where
 		u = toUUID f
diff --git a/Git/Repair.hs b/Git/Repair.hs
--- a/Git/Repair.hs
+++ b/Git/Repair.hs
@@ -325,7 +325,11 @@
  - the commit. Also adds to a set of commit shas that have been verified to
  - be good, which can be passed into subsequent calls to avoid
  - redundant work when eg, chasing down branches to find the first
- - uncorrupted commit. -}
+ - uncorrupted commit.
+ -
+ - When the sha is not a commit but some other git object, returns
+ - true, but does not add it to the set.
+ -}
 verifyCommit :: MissingObjects -> GoodCommits -> Sha -> Repo -> IO (Bool, GoodCommits)
 verifyCommit missing goodcommits commit r
 	| checkGoodCommit commit goodcommits = return (True, goodcommits)
@@ -337,16 +341,23 @@
 			, Param (fromRef commit)
 			] r
 		let committrees = map (parse . decodeBL) ls
-		if any isNothing committrees || null committrees
-			then do
-				void cleanup
-				return (False, goodcommits)
-			else do
-				let cts = catMaybes committrees
-				ifM (cleanup <&&> check cts)
-					( return (True, addGoodCommits (map fst cts) goodcommits)
-					, return (False, goodcommits)
-					)
+		-- git log on an object that is not a commit will
+		-- succeed without any output
+		if null committrees
+			then ifM cleanup
+				( return (True, goodcommits)
+				, return (False, goodcommits)
+				)
+			else if any isNothing committrees
+				then do
+					void cleanup
+					return (False, goodcommits)
+				else do
+					let cts = catMaybes committrees
+					ifM (cleanup <&&> check cts)
+						( return (True, addGoodCommits (map fst cts) goodcommits)
+						, return (False, goodcommits)
+						)
   where
 	parse l = case words l of
 		(commitsha:treesha:[]) -> (,)
diff --git a/Limit.hs b/Limit.hs
--- a/Limit.hs
+++ b/Limit.hs
@@ -397,7 +397,7 @@
 	Nothing -> Left "bad value for number of lacking copies"
   where
 	go mi needed notpresent key = do
-		NumCopies numcopies <- if approx
+		numcopies <- if approx
 			then approxNumCopies
 			else case mi of
 				MatchingFile fi -> getGlobalFileNumCopies $
@@ -406,7 +406,7 @@
 				MatchingUserInfo {} -> approxNumCopies
 		us <- filter (`S.notMember` notpresent)
 			<$> (trustExclude UnTrusted =<< Remote.keyLocations key)
-		return $ numcopies - length us >= needed
+		return $ fromNumCopies numcopies - length us >= needed
 	approxNumCopies = fromMaybe defaultNumCopies <$> getGlobalNumCopies
 
 {- Match keys that are unused.
diff --git a/Logs/NumCopies.hs b/Logs/NumCopies.hs
--- a/Logs/NumCopies.hs
+++ b/Logs/NumCopies.hs
@@ -24,12 +24,12 @@
 import Logs.SingleValue
 
 instance SingleValueSerializable NumCopies where
-	serialize (NumCopies n) = encodeBS (show n)
-	deserialize = NumCopies <$$> readish . decodeBS
+	serialize = encodeBS . show . fromNumCopies
+	deserialize = configuredNumCopies <$$> readish . decodeBS
 
 instance SingleValueSerializable MinCopies where
-	serialize (MinCopies n) = encodeBS (show n)
-	deserialize = MinCopies <$$> readish . decodeBS
+	serialize = encodeBS . show . fromMinCopies
+	deserialize = configuredMinCopies <$$> readish . decodeBS
 
 setGlobalNumCopies :: NumCopies -> Annex ()
 setGlobalNumCopies new = do
diff --git a/Remote/GCrypt.hs b/Remote/GCrypt.hs
--- a/Remote/GCrypt.hs
+++ b/Remote/GCrypt.hs
@@ -130,7 +130,8 @@
 	cst <- remoteCost gc $
 		if repoCheap r then nearlyCheapRemoteCost else expensiveRemoteCost
 	let (rsynctransport, rsyncurl, accessmethod) = rsyncTransportToObjects r gc
-	let rsyncopts = Remote.Rsync.genRsyncOpts c gc rsynctransport rsyncurl
+	protectsargs <- liftIO Remote.Rsync.probeRsyncProtectsArgs
+	let rsyncopts = Remote.Rsync.genRsyncOpts protectsargs c gc rsynctransport rsyncurl
 	let this = Remote 
 		{ uuid = u
 		, cost = cst
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -16,7 +16,8 @@
 	withRsyncScratchDir,
 	rsyncRemoteConfigs,
 	genRsyncOpts,
-	RsyncOpts
+	RsyncOpts,
+	probeRsyncProtectsArgs,
 ) where
 
 import Annex.Common
@@ -36,6 +37,7 @@
 import Crypto
 import Utility.Rsync
 import Utility.CopyFile
+import Utility.Process.Transcript
 import Messages.Progress
 import Utility.Metered
 import Types.Transfer
@@ -74,7 +76,8 @@
 	cst <- remoteCost gc expensiveRemoteCost
 	(transport, url) <- rsyncTransport gc $
 		fromMaybe (giveup "missing rsyncurl") $ remoteAnnexRsyncUrl gc
-	let o = genRsyncOpts c gc transport url
+	protectsargs <- liftIO probeRsyncProtectsArgs
+	let o = genRsyncOpts protectsargs c gc transport url
 	let islocal = rsyncUrlIsPath $ rsyncUrl o
 	return $ Just $ specialRemote c
 		(fileStorer $ store o)
@@ -124,6 +127,18 @@
 			, remoteStateHandle = rs
 			}
 
+-- | Since 3.2.4, rsync protects filenames from being exposed to the shell.
+newtype RsyncProtectsArgs = RsyncProtectsArgs Bool
+
+probeRsyncProtectsArgs :: IO RsyncProtectsArgs
+probeRsyncProtectsArgs = do
+	(helpoutput, _) <- processTranscript "rsync" ["--help"] Nothing
+	-- The --old-args option was added to disable the new arg
+	-- protection, so use it to detect when that feature is supported
+	-- by rsync, rather than parsing versions.
+	return (RsyncProtectsArgs $ "--old-args" `isInfixOf` helpoutput)
+
+
 -- Things used by genRsyncOpts
 rsyncRemoteConfigs :: [RemoteConfigFieldParser]
 rsyncRemoteConfigs = 
@@ -131,15 +146,17 @@
 		(FieldDesc "set to no to avoid usual shell escaping (not recommended)")
 	]
 
-genRsyncOpts :: ParsedRemoteConfig -> RemoteGitConfig -> Annex [CommandParam] -> RsyncUrl -> RsyncOpts
-genRsyncOpts c gc transport url = RsyncOpts
+genRsyncOpts :: RsyncProtectsArgs -> ParsedRemoteConfig -> RemoteGitConfig -> Annex [CommandParam] -> RsyncUrl -> RsyncOpts
+genRsyncOpts (RsyncProtectsArgs protectsargs) c gc transport url = RsyncOpts
 	{ rsyncUrl = url
 	, rsyncOptions = appendtransport $ opts []
 	, rsyncUploadOptions = appendtransport $
 		opts (remoteAnnexRsyncUploadOptions gc)
 	, rsyncDownloadOptions = appendtransport $
 		opts (remoteAnnexRsyncDownloadOptions gc)
-	, rsyncShellEscape = fromMaybe True (getRemoteConfigValue shellEscapeField c)
+	, rsyncShellEscape = if protectsargs
+		then False
+		else fromMaybe True (getRemoteConfigValue shellEscapeField c)
 	}
   where
 	appendtransport l = (++ l) <$> transport
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -1794,7 +1794,12 @@
 		-- it needs to be able to store the agent socket there,
 		-- which can be problimatic when testing some filesystems.
 		absgpgtmp <- fromRawFilePath <$> absPath (toRawFilePath gpgtmp)
-		testscheme' scheme absgpgtmp
+		res <- testscheme' scheme absgpgtmp
+		-- gpg may still be running and would prevent
+		-- removeDirectoryRecursive from succeeding, so
+		-- force removal of the temp directory.
+		liftIO $ removeDirectoryForCleanup gpgtmp
+		return res
 	testscheme' scheme absgpgtmp = intmpclonerepo $ do
 		-- Since gpg uses a unix socket, which is limited to a
 		-- short path, use whichever is shorter of absolute
diff --git a/Test/Framework.hs b/Test/Framework.hs
--- a/Test/Framework.hs
+++ b/Test/Framework.hs
@@ -743,18 +743,16 @@
 		nvar <- newTVarIO (1, length ts)
 		exitcodes <- forConcurrently [1..numjobs] $ \_ -> 
 			worker [] nvar runone
-		let exitcodes' = concat exitcodes
 		unless (keepFailuresOption opts) finalCleanup
-		if all (== ExitSuccess) exitcodes'
-			then exitSuccess
-			else case (filter (/= ExitFailure 1) exitcodes') of
-				[] -> do
-					putStrLn "  (Failures above could be due to a bug in git-annex, or an incompatibility"
-					putStrLn "   with utilities, such as git, installed on this system.)"
-					exitFailure
-				v -> do
-					putStrLn $ "  Test subprocesses exited with unexpected exit codes: " ++ show v
-					exitFailure
+		case nub (filter (/= ExitSuccess) (concat exitcodes)) of
+			[] -> exitSuccess
+			[ExitFailure 1] -> do
+				putStrLn "  (Failures above could be due to a bug in git-annex, or an incompatibility"
+				putStrLn "   with utilities, such as git, installed on this system.)"
+				exitFailure
+			_ -> do
+				putStrLn $ "  Test subprocesses exited with unexpected exit codes: " ++ show (concat exitcodes)
+				exitFailure
 	go (Just subenvval) = case readish subenvval of
 		Nothing -> error ("Bad " ++ subenv)
 		Just (n, crippledfilesystem, adjustedbranchok) -> isolateGitConfig $ do
diff --git a/Types/Export.hs b/Types/Export.hs
--- a/Types/Export.hs
+++ b/Types/Export.hs
@@ -21,7 +21,7 @@
 import Utility.Split
 import Utility.FileSystemEncoding
 
-import Data.ByteString.Short as S
+import qualified Data.ByteString.Short as S
 import qualified System.FilePath.Posix as Posix
 import GHC.Generics
 import Control.DeepSeq
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -148,7 +148,8 @@
 extractGitConfig configsource r = GitConfig
 	{ annexVersion = RepoVersion <$> getmayberead (annexConfig "version")
 	, annexUUID = hereuuid
-	, annexNumCopies = NumCopies <$> getmayberead (annexConfig "numcopies")
+	, annexNumCopies = configuredNumCopies
+		<$> getmayberead (annexConfig "numcopies")
 	, annexDiskReserve = fromMaybe onemegabyte $
 		readSize dataUnits =<< getmaybe (annexConfig "diskreserve")
 	, annexDirect = getbool (annexConfig "direct") False
diff --git a/Types/NumCopies.hs b/Types/NumCopies.hs
--- a/Types/NumCopies.hs
+++ b/Types/NumCopies.hs
@@ -1,14 +1,16 @@
 {- git-annex numcopies types
  -
- - Copyright 2014-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2014-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 module Types.NumCopies (
-	NumCopies(..),
+	NumCopies,
+	configuredNumCopies,
 	fromNumCopies,
-	MinCopies(..),
+	MinCopies,
+	configuredMinCopies,
 	fromMinCopies,
 	VerifiedCopy(..),
 	checkVerifiedCopy,
@@ -38,11 +40,23 @@
 newtype NumCopies = NumCopies Int
 	deriving (Ord, Eq, Show)
 
+-- Smart constructor; prevent configuring numcopies to 0 which would
+-- cause data loss.
+configuredNumCopies :: Int -> NumCopies
+configuredNumCopies n
+	| n > 0 = NumCopies n
+	| otherwise = NumCopies 1
+
 fromNumCopies :: NumCopies -> Int
 fromNumCopies (NumCopies n) = n
 
 newtype MinCopies = MinCopies Int
 	deriving (Ord, Eq, Show)
+
+configuredMinCopies :: Int -> MinCopies
+configuredMinCopies n
+	| n > 0 = MinCopies n
+	| otherwise = MinCopies 1
 
 fromMinCopies :: MinCopies -> Int
 fromMinCopies (MinCopies n) = n
diff --git a/Utility/Tmp/Dir.hs b/Utility/Tmp/Dir.hs
--- a/Utility/Tmp/Dir.hs
+++ b/Utility/Tmp/Dir.hs
@@ -1,6 +1,6 @@
 {- Temporary directories
  -
- - Copyright 2010-2013 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2022 Joey Hess <id@joeyh.name>
  -
  - License: BSD-2-clause
  -}
@@ -63,8 +63,10 @@
 	-- after a process has just written to it and exited.
 	-- Because it's crap, presumably. So, ignore failure
 	-- to delete the temp directory.
-	_ <- tryIO $ removeDirectoryRecursive tmpdir
+	_ <- tryIO $ go tmpdir
 	return ()
 #else
-	removeDirectoryRecursive tmpdir
+	go tmpdir
 #endif
+  where
+	go = removeDirectoryRecursive
diff --git a/doc/git-annex-config.mdwn b/doc/git-annex-config.mdwn
--- a/doc/git-annex-config.mdwn
+++ b/doc/git-annex-config.mdwn
@@ -31,23 +31,23 @@
 
 * `annex.largefiles`
 
-   Used to configure which files are large enough to be added to the annex.
-   It is an expression that matches the large files, eg
-   "`include=*.mp3 or largerthan(500kb)`".
-   See [[git-annex-matching-expression]](1) for details on the syntax.
+  Used to configure which files are large enough to be added to the annex.
+  It is an expression that matches the large files, eg
+  "`include=*.mp3 or largerthan(500kb)`".
+  See [[git-annex-matching-expression]](1) for details on the syntax.
 
-   This sets a default, which can be overridden by annex.largefiles
-   attributes in `.gitattributes` files, or by `git config`.
+  This sets a default, which can be overridden by annex.largefiles
+  attributes in `.gitattributes` files, or by `git config`.
 
 * `annex.dotfiles`
 
-   Normally, dotfiles are assumed to be files like .gitignore,
-   whose content should always be part of the git repository, so 
-   they will not be added to the annex. Setting annex.dotfiles to true
-   makes dotfiles be added to the annex the same as any other file. 
+  Normally, dotfiles are assumed to be files like .gitignore,
+  whose content should always be part of the git repository, so 
+  they will not be added to the annex. Setting annex.dotfiles to true
+  makes dotfiles be added to the annex the same as any other file. 
 
-   This sets a default, which can be overridden by annex.dotfiles
-   in `git config`.
+  This sets a default, which can be overridden by annex.dotfiles
+  in `git config`.
 
 * `annex.addunlocked`
 
@@ -66,6 +66,9 @@
 
   Set to false to prevent the `git-annex assistant` and `git-annex sync`
   from automatically committing changes to files in the repository.
+   
+  This sets a default, which can be overridden by annex.autocommit
+  in `git config`.
 
 * `annex.resolvemerge`
 
@@ -73,15 +76,24 @@
   being automatically resolved by the `git-annex assitant`,
   `git-annex sync`, `git-annex merge`, and the `git-annex post-receive`
   hook.
+   
+  This sets a default, which can be overridden by annex.resolvemerge
+  in `git config`.
 
 * `annex.synccontent`
 
   Set to true to make git-annex sync default to syncing annexed content.
+  
+  This sets a default, which can be overridden by annex.synccontent
+  in `git config`.
 
 * `annex.synconlyannex`
 
   Set to true to make git-annex sync default to only sync the git-annex
   branch and annexed content.
+  
+  This sets a default, which can be overridden by annex.synconlyannex
+  in `git config`.
 
 * `annex.securehashesonly`
 
diff --git a/doc/git-annex-forget.mdwn b/doc/git-annex-forget.mdwn
--- a/doc/git-annex-forget.mdwn
+++ b/doc/git-annex-forget.mdwn
@@ -32,6 +32,8 @@
 
 [[git-annex]](1)
 
+[[git-annex-dead]](1)
+
 [[git-annex-filter-branch]](1)
 
 # AUTHOR
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -932,8 +932,6 @@
   never been configured, and when there's no annex.numcopies setting in the
   .gitattributes file.
 
-  Note that setting numcopies to 0 is very unsafe.
-
 * `annex.genmetadata`
 
   Set this to `true` to make git-annex automatically generate some metadata
@@ -1880,8 +1878,6 @@
 
 	*.wav annex.numcopies=2
 	*.flac annex.numcopies=3
-
-Note that setting numcopies or mincopies to 0 is very unsafe.
 
 These settings are honored by git-annex whenever it's operating on a
 matching file. However, when using --all, --unused, or --key to specify
diff --git a/git-annex.cabal b/git-annex.cabal
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 10.20220322
+Version: 10.20220504
 Cabal-Version: >= 1.10
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>
