diff --git a/Annex/AdjustedBranch.hs b/Annex/AdjustedBranch.hs
--- a/Annex/AdjustedBranch.hs
+++ b/Annex/AdjustedBranch.hs
@@ -558,8 +558,8 @@
 reverseAdjustedTree :: Sha -> Adjustment -> Sha -> Annex Sha
 reverseAdjustedTree basis adj csha = do
 	(diff, cleanup) <- inRepo (Git.DiffTree.commitDiff csha)
-	let (adds, others) = partition (\dti -> Git.DiffTree.srcsha dti == nullSha) diff
-	let (removes, changes) = partition (\dti -> Git.DiffTree.dstsha dti == nullSha) others
+	let (adds, others) = partition (\dti -> Git.DiffTree.srcsha dti `elem` nullShas) diff
+	let (removes, changes) = partition (\dti -> Git.DiffTree.dstsha dti `elem` nullShas) others
 	adds' <- catMaybes <$>
 		mapM (adjustTreeItem reverseadj) (map diffTreeToTreeItem adds)
 	treesha <- Git.Tree.adjustTree
diff --git a/Annex/Branch/Transitions.hs b/Annex/Branch/Transitions.hs
--- a/Annex/Branch/Transitions.hs
+++ b/Annex/Branch/Transitions.hs
@@ -22,6 +22,7 @@
 import Types.UUID
 import Types.MetaData
 import Types.Remote
+import Types.ProposedAccepted
 import Annex.SpecialRemote.Config
 
 import qualified Data.Map as M
@@ -85,7 +86,7 @@
 	trustmap' = trustmap `M.union`
 		M.map (const DeadTrusted) (M.filter sameasdead remoteconfigmap)
 	sameasdead cm =
-		case toUUID <$> M.lookup sameasUUIDField cm of
+		case toUUID . fromProposedAccepted <$> M.lookup sameasUUIDField cm of
 			Nothing -> False
 			Just u' -> M.lookup u' trustmap == Just DeadTrusted
 	minimizesameasdead u l
diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -780,13 +780,12 @@
 
 {- Downloads content from any of a list of urls, displaying a progress
  - meter. -}
-downloadUrl :: Key -> MeterUpdate -> [Url.URLString] -> FilePath -> Annex Bool
-downloadUrl k p urls file = 
+downloadUrl :: Key -> MeterUpdate -> [Url.URLString] -> FilePath -> Url.UrlOptions -> Annex Bool
+downloadUrl k p urls file uo = 
 	-- Poll the file to handle configurations where an external
 	-- download command is used.
 	meteredFile file (Just p) k $
-		Url.withUrlOptions $ \uo -> 
-			anyM (\u -> Url.download p u file uo) urls
+		anyM (\u -> Url.download p u file uo) urls
 
 {- Copies a key's content, when present, to a temp file.
  - This is used to speed up some rsyncs. -}
diff --git a/Annex/Content/PointerFile.hs b/Annex/Content/PointerFile.hs
--- a/Annex/Content/PointerFile.hs
+++ b/Annex/Content/PointerFile.hs
@@ -22,7 +22,9 @@
 import Annex.InodeSentinal
 import Annex.Content.LowLevel
 import Utility.InodeCache
+#if ! defined(mingw32_HOST_OS)
 import Utility.Touch
+#endif
 
 {- Populates a pointer file with the content of a key. 
  -
diff --git a/Annex/Drop.hs b/Annex/Drop.hs
--- a/Annex/Drop.hs
+++ b/Annex/Drop.hs
@@ -16,7 +16,7 @@
 import qualified Command.Drop
 import Command
 import Annex.Wanted
-import Config
+import Annex.SpecialRemote.Config
 import qualified Database.Keys
 import Git.FilePath
 
diff --git a/Annex/FileMatcher.hs b/Annex/FileMatcher.hs
--- a/Annex/FileMatcher.hs
+++ b/Annex/FileMatcher.hs
@@ -37,8 +37,10 @@
 import Types.FileMatcher
 import Types.GitConfig
 import Config.GitConfig
+import Annex.SpecialRemote.Config (preferreddirField)
 import Git.FilePath
 import Types.Remote (RemoteConfig)
+import Types.ProposedAccepted
 import Annex.CheckAttr
 import Git.CheckAttr (unspecifiedAttr)
 import qualified Git.Config
@@ -155,8 +157,8 @@
 	, SimpleToken "inpreferreddir" (simply $ limitInDir preferreddir)
 	] ++ commonKeylessTokens LimitAnnexFiles
   where
-	preferreddir = fromMaybe "public" $
-		M.lookup "preferreddir" =<< (`M.lookup` configMap pcd) =<< repoUUID pcd
+	preferreddir = maybe "public" fromProposedAccepted $
+		M.lookup preferreddirField =<< (`M.lookup` configMap pcd) =<< repoUUID pcd
 
 preferredContentKeyedTokens :: PreferredContentData -> [ParseToken (MatchFiles Annex)]
 preferredContentKeyedTokens pcd =
diff --git a/Annex/Import.hs b/Annex/Import.hs
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -8,7 +8,6 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 module Annex.Import (
-	importTree,
 	ImportTreeConfig(..),
 	ImportCommitConfig(..),
 	buildImportCommit,
@@ -37,7 +36,6 @@
 import Annex.RemoteTrackingBranch
 import Command
 import Backend
-import Config
 import Types.Key
 import Types.KeySource
 import Messages.Progress
diff --git a/Annex/SpecialRemote.hs b/Annex/SpecialRemote.hs
--- a/Annex/SpecialRemote.hs
+++ b/Annex/SpecialRemote.hs
@@ -17,6 +17,7 @@
 import Remote (remoteTypes)
 import Types.Remote (RemoteConfig, SetupStage(..), typename, setup)
 import Types.GitConfig
+import Types.ProposedAccepted
 import Config
 import Remote.List
 import Logs.Remote
@@ -49,10 +50,10 @@
 	-- when sameas is used
 	-> RemoteConfig
 newConfig name sameas fromuser m = case sameas of
-	Nothing -> M.insert nameField name fromuser
+	Nothing -> M.insert nameField (Proposed name) fromuser
 	Just (Sameas u) -> addSameasInherited m $ M.fromList
-		[ (sameasNameField, name)
-		, (sameasUUIDField, fromUUID u)
+		[ (sameasNameField, Proposed name)
+		, (sameasUUIDField, Proposed (fromUUID u))
 		] `M.union` fromuser
 
 specialRemoteMap :: Annex (M.Map UUID RemoteName)
@@ -66,11 +67,15 @@
 
 {- find the remote type -}
 findType :: RemoteConfig -> Either String RemoteType
-findType config = maybe unspecified specified $ M.lookup typeField config
+findType config = maybe unspecified (specified . fromProposedAccepted) $
+	M.lookup typeField config
   where
 	unspecified = Left "Specify the type of remote with type="
 	specified s = case filter (findtype s) remoteTypes of
-		[] -> Left $ "Unknown remote type " ++ s
+		[] -> Left $ "Unknown remote type " ++ s 
+			++ " (pick from: "
+			++ intercalate " " (map typename remoteTypes)
+			++ ")"
 		(t:_) -> Right t
 	findtype s i = typename i == s
 
@@ -94,7 +99,8 @@
 			_ -> return ()
   where
 	configured rc = fromMaybe False $
-		Git.Config.isTrueFalse =<< M.lookup autoEnableField rc
+		Git.Config.isTrueFalse . fromProposedAccepted
+			=<< M.lookup autoEnableField rc
 	canenable u = (/= DeadTrusted) <$> lookupTrust u
 	getenabledremotes = M.fromList
 		. map (\r -> (getcu r, r))
diff --git a/Annex/SpecialRemote/Config.hs b/Annex/SpecialRemote/Config.hs
--- a/Annex/SpecialRemote/Config.hs
+++ b/Annex/SpecialRemote/Config.hs
@@ -1,18 +1,27 @@
 {- git-annex special remote configuration
  -
- - Copyright 2019 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
 module Annex.SpecialRemote.Config where
 
 import Common
 import Types.Remote (RemoteConfigField, RemoteConfig)
 import Types.UUID
+import Types.ProposedAccepted
+import Types.RemoteConfig
+import Config
+import qualified Git.Config
 
 import qualified Data.Map as M
 import qualified Data.Set as S
+import Data.Typeable
+import GHC.Stack
 
 newtype Sameas t = Sameas t
 	deriving (Show)
@@ -22,45 +31,85 @@
 
 {- The name of a configured remote is stored in its config using this key. -}
 nameField :: RemoteConfigField
-nameField = "name"
+nameField = Accepted "name"
 
 {- The name of a sameas remote is stored using this key instead. 
  - This prevents old versions of git-annex getting confused. -}
 sameasNameField :: RemoteConfigField
-sameasNameField = "sameas-name"
+sameasNameField = Accepted "sameas-name"
 
 lookupName :: RemoteConfig -> Maybe String
-lookupName c = M.lookup nameField c <|> M.lookup sameasNameField c
+lookupName c = fmap fromProposedAccepted $
+	M.lookup nameField c <|> M.lookup sameasNameField c
 
+instance RemoteNameable RemoteConfig where
+	getRemoteName c = fromMaybe "" (lookupName c)
+
 {- The uuid that a sameas remote is the same as is stored in this key. -}
 sameasUUIDField :: RemoteConfigField
-sameasUUIDField = "sameas-uuid"
+sameasUUIDField = Accepted "sameas-uuid"
 
 {- The type of a remote is stored in its config using this key. -}
 typeField :: RemoteConfigField
-typeField = "type"
+typeField = Accepted "type"
 
 autoEnableField :: RemoteConfigField
-autoEnableField = "autoenable"
+autoEnableField = Accepted "autoenable"
 
 encryptionField :: RemoteConfigField
-encryptionField = "encryption"
+encryptionField = Accepted "encryption"
 
 macField :: RemoteConfigField
-macField = "mac"
+macField = Accepted "mac"
 
 cipherField :: RemoteConfigField
-cipherField = "cipher"
+cipherField = Accepted "cipher"
 
 cipherkeysField :: RemoteConfigField
-cipherkeysField = "cipherkeys"
+cipherkeysField = Accepted "cipherkeys"
 
 pubkeysField :: RemoteConfigField
-pubkeysField = "pubkeys"
+pubkeysField = Accepted "pubkeys"
 
+chunkField :: RemoteConfigField
+chunkField = Accepted "chunk"
+
 chunksizeField :: RemoteConfigField
-chunksizeField = "chunksize"
+chunksizeField = Accepted "chunksize"
 
+embedCredsField :: RemoteConfigField
+embedCredsField = Accepted "embedcreds"
+
+preferreddirField :: RemoteConfigField
+preferreddirField = Accepted "preferreddir"
+
+exportTreeField :: RemoteConfigField
+exportTreeField = Accepted "exporttree"
+
+importTreeField :: RemoteConfigField
+importTreeField = Accepted "importtree"
+
+exportTree :: ParsedRemoteConfig -> Bool
+exportTree = fromMaybe False . getRemoteConfigValue exportTreeField
+
+importTree :: ParsedRemoteConfig -> Bool
+importTree = fromMaybe False . getRemoteConfigValue importTreeField
+
+{- Parsers for fields that are common to all special remotes. -}
+commonFieldParsers :: [RemoteConfigFieldParser]
+commonFieldParsers =
+	[ optionalStringParser nameField
+		(FieldDesc "name for the special remote")
+	, optionalStringParser sameasNameField HiddenField
+	, optionalStringParser sameasUUIDField HiddenField
+	, optionalStringParser typeField
+		(FieldDesc "type of special remote")
+	, trueFalseParser autoEnableField False
+		(FieldDesc "automatically enable special remote")
+	, optionalStringParser preferreddirField
+		(FieldDesc "directory whose content is preferred")
+	]
+
 {- A remote with sameas-uuid set will inherit these values from the config
  - of that uuid. These values cannot be overridden in the remote's config. -}
 sameasInherits :: S.Set RemoteConfigField
@@ -92,7 +141,8 @@
 			M.restrictKeys parentc sameasInherits
 
 findSameasUUID :: RemoteConfig -> Maybe (Sameas UUID)
-findSameasUUID c = Sameas . toUUID <$> M.lookup sameasUUIDField c
+findSameasUUID c = Sameas . toUUID . fromProposedAccepted
+	<$> M.lookup sameasUUIDField c
 
 {- Remove any fields inherited from a sameas-uuid. When storing a
  - RemoteConfig, those fields don't get stored, since they were already
@@ -108,4 +158,98 @@
   where
 	sameasuuid (u, c) = case M.lookup sameasUUIDField c of
 		Nothing -> (u, c, Nothing)
-		Just u' -> (toUUID u', c, Just (ConfigFrom u))
+		Just u' -> (toUUID (fromProposedAccepted u'), c, Just (ConfigFrom u))
+
+{- Extracts a value from ParsedRemoteConfig. -}
+getRemoteConfigValue :: HasCallStack => Typeable v => RemoteConfigField -> ParsedRemoteConfig -> Maybe v
+getRemoteConfigValue f m = case M.lookup f m of
+	Just (RemoteConfigValue v) -> case cast v of
+		Just v' -> Just v'
+		Nothing -> error $ unwords
+			[ "getRemoteConfigValue"
+			, fromProposedAccepted f
+			, "found value of unexpected type"
+			, show (typeOf v) ++ "."
+			, "This is a bug in git-annex!"
+			]
+	Nothing -> Nothing
+
+{- Gets all fields that remoteConfigRestPassthrough matched. -}
+getRemoteConfigPassedThrough :: ParsedRemoteConfig -> M.Map RemoteConfigField String
+getRemoteConfigPassedThrough = M.mapMaybe $ \(RemoteConfigValue v) ->
+	case cast v of
+		Just (PassedThrough s) -> Just s
+		Nothing -> Nothing
+
+newtype PassedThrough = PassedThrough String
+
+parseRemoteConfig :: RemoteConfig -> RemoteConfigParser -> Either String ParsedRemoteConfig
+parseRemoteConfig c rpc =
+	go [] c (remoteConfigFieldParsers rpc ++ commonFieldParsers)
+  where
+	go l c' [] =
+		let (passover, leftovers) = partition
+			(maybe (const False) fst (remoteConfigRestPassthrough rpc) . fst)
+			(M.toList c')
+		    leftovers' = filter (notaccepted . fst) leftovers
+		in if not (null leftovers')
+			then Left $ "Unexpected parameters: " ++
+				unwords (map (fromProposedAccepted . fst) leftovers')
+			else Right $ M.fromList $
+				l ++ map (uncurry passthrough) passover
+	go l c' (p:rest) = do
+		let f = parserForField p
+		(valueParser p) (M.lookup f c) c >>= \case
+			Just v -> go ((f,v):l) (M.delete f c') rest
+			Nothing -> go l (M.delete f c') rest
+	
+	passthrough f v = (f, RemoteConfigValue (PassedThrough (fromProposedAccepted v)))
+	
+	notaccepted (Proposed _) = True
+	notaccepted (Accepted _) = False
+
+optionalStringParser :: RemoteConfigField -> FieldDesc -> RemoteConfigFieldParser
+optionalStringParser f fielddesc = RemoteConfigFieldParser
+	{ parserForField = f 
+	, valueParser = p
+	, fieldDesc = fielddesc
+	, valueDesc = Nothing
+	}
+  where
+	p (Just v) _c = Right (Just (RemoteConfigValue (fromProposedAccepted v)))
+	p Nothing _c = Right Nothing
+
+yesNoParser :: RemoteConfigField -> Bool -> FieldDesc -> RemoteConfigFieldParser
+yesNoParser f v fd = genParser yesNo f v fd
+	(Just (ValueDesc "yes or no"))
+
+trueFalseParser :: RemoteConfigField -> Bool -> FieldDesc -> RemoteConfigFieldParser
+trueFalseParser f v fd = genParser Git.Config.isTrueFalse f v fd
+	(Just (ValueDesc "true or false"))
+
+genParser
+	:: Typeable t
+	=> (String -> Maybe t)
+	-> RemoteConfigField
+	-> t -- ^ fallback value
+	-> FieldDesc
+	-> Maybe ValueDesc
+	-> RemoteConfigFieldParser
+genParser parse f fallback fielddesc valuedesc = RemoteConfigFieldParser	
+	{ parserForField = f
+	, valueParser = p
+	, fieldDesc = fielddesc
+	, valueDesc = valuedesc
+	}
+  where
+	p Nothing _c = Right (Just (RemoteConfigValue fallback))
+	p (Just v) _c = case parse (fromProposedAccepted v) of
+		Just b -> Right (Just (RemoteConfigValue b))
+		Nothing -> case v of
+			Accepted _ -> Right (Just (RemoteConfigValue fallback))
+			Proposed _ -> Left $
+				"Bad value for " ++ fromProposedAccepted f ++
+				case valuedesc of
+					Just (ValueDesc vd) ->
+						" (expected " ++ vd ++ ")"
+					Nothing -> ""
diff --git a/Annex/Ssh.hs b/Annex/Ssh.hs
--- a/Annex/Ssh.hs
+++ b/Annex/Ssh.hs
@@ -1,6 +1,6 @@
 {- git-annex ssh interface, with connection caching
  -
- - Copyright 2012-2017 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -100,11 +100,44 @@
 sshCachingInfo :: (SshHost, Maybe Integer) -> Annex (Maybe FilePath, [CommandParam])
 sshCachingInfo (host, port) = go =<< sshCacheDir
   where
-	go Nothing = return (Nothing, [])
 	go (Just dir) =
 		liftIO (bestSocketPath $ dir </> hostport2socket host port) >>= return . \case
 			Nothing -> (Nothing, [])
 			Just socketfile -> (Just socketfile, sshConnectionCachingParams socketfile)
+	-- No connection caching with concurrency is not a good
+	-- combination, so warn the user.
+	go Nothing = do
+		Annex.getState Annex.concurrency >>= \case
+                	NonConcurrent -> return ()
+			Concurrent {} -> warnnocaching
+			ConcurrentPerCpu -> warnnocaching
+		return (Nothing, [])
+	
+	warnnocaching = do
+		warning nocachingwarning
+		ifM (fromMaybe True . annexSshCaching <$> Annex.getGitConfig)
+			( whenM crippledFileSystem $
+				warning crippledfswarning
+			, warning enablecachingwarning
+			)
+	
+	nocachingwarning = unwords
+		[ "You have enabled concurrency, but ssh connection caching"
+		, "is not enabled. This may result in multiple ssh processes"
+		, "prompting for passwords at the same time."
+		]
+	
+	crippledfswarning = unwords
+		[ "This repository is on a crippled filesystem, so unix named"
+		, "pipes probably don't work, and ssh connection caching"
+		, "relies on those. One workaround is to set"
+		, sshSocketDirEnv
+		, "to point to a directory on a non-crippled filesystem."
+		, "(Or, disable concurrency.)"
+		]
+	
+	enablecachingwarning = 
+		"Enable annex.sshcaching (or disable concurrency) to avoid this problem."
 
 {- Given an absolute path to use for a socket file,
  - returns whichever is shorter of that or the relative path to the same
@@ -133,9 +166,11 @@
 	, Param "-o", Param "ControlPersist=yes"
 	]
 
+sshSocketDirEnv :: String
+sshSocketDirEnv = "GIT_ANNEX_SSH_SOCKET_DIR"
+
 {- ssh connection caching creates sockets, so will not work on a
- - crippled filesystem. A GIT_ANNEX_TMP_DIR can be provided to use
- - a different filesystem. -}
+ - crippled filesystem. -}
 sshCacheDir :: Annex (Maybe FilePath)
 sshCacheDir
 	| BuildInfo.sshconnectioncaching = 
@@ -148,7 +183,7 @@
 			)
 	| otherwise = return Nothing
   where
-	gettmpdir = liftIO $ getEnv "GIT_ANNEX_TMP_DIR"
+	gettmpdir = liftIO $ getEnv sshSocketDirEnv
 	usetmpdir tmpdir = liftIO $ catchMaybeIO $ do
 		let socktmp = tmpdir </> "ssh"
 		createDirectoryIfMissing True socktmp
diff --git a/Annex/Url.hs b/Annex/Url.hs
--- a/Annex/Url.hs
+++ b/Annex/Url.hs
@@ -1,13 +1,14 @@
 {- Url downloading, with git-annex user agent and configured http
  - headers, security restrictions, etc.
  -
- - Copyright 2013-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2013-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 module Annex.Url (
 	withUrlOptions,
+	withUrlOptionsPromptingCreds,
 	getUrlOptions,
 	getUserAgent,
 	ipAddressesUnlimited,
@@ -34,6 +35,7 @@
 import Utility.IPAddress
 import Utility.HttpManagerRestricted
 import Utility.Metered
+import Git.Credential
 import qualified BuildInfo
 
 import Network.Socket
@@ -64,6 +66,7 @@
 			<*> pure urldownloader
 			<*> pure manager
 			<*> (annexAllowedUrlSchemes <$> Annex.getGitConfig)
+			<*> pure U.noBasicAuth
 	
 	headers = annexHttpHeadersCommand <$> Annex.getGitConfig >>= \case
 		Just cmd -> lines <$> liftIO (readProcess "sh" ["-c", cmd])
@@ -123,6 +126,24 @@
 
 withUrlOptions :: (U.UrlOptions -> Annex a) -> Annex a
 withUrlOptions a = a =<< getUrlOptions
+
+-- When downloading an url, if authentication is needed, uses
+-- git-credential to prompt for username and password.
+withUrlOptionsPromptingCreds :: (U.UrlOptions -> Annex a) -> Annex a
+withUrlOptionsPromptingCreds a = do
+	g <- Annex.gitRepo
+	uo <- getUrlOptions
+	prompter <- mkPrompter
+	a $ uo
+		{ U.getBasicAuth = \u -> prompter $
+			getBasicAuthFromCredential g u
+		-- Can't download with curl and handle basic auth,
+		-- so make sure it uses conduit.
+		, U.urlDownloader = case U.urlDownloader uo of
+			U.DownloadWithCurl _ -> U.DownloadWithConduit $
+				U.DownloadWithCurlRestricted mempty
+			v -> v
+		}
 
 checkBoth :: U.URLString -> Maybe Integer -> U.UrlOptions -> Annex Bool
 checkBoth url expected_size uo =
diff --git a/Annex/View.hs b/Annex/View.hs
--- a/Annex/View.hs
+++ b/Annex/View.hs
@@ -396,12 +396,12 @@
 	void $ liftIO cleanup
   where
 	handleremovals item
-		| DiffTree.srcsha item /= nullSha =
+		| DiffTree.srcsha item `notElem` nullShas =
 			handlechange item removemeta
 				=<< catKey (DiffTree.srcsha item)
 		| otherwise = noop
 	handleadds item
-		| DiffTree.dstsha item /= nullSha = 
+		| DiffTree.dstsha item `notElem` nullShas = 
 			handlechange item addmeta
 				=<< catKey (DiffTree.dstsha item)
 		| otherwise = noop
diff --git a/Assistant/DaemonStatus.hs b/Assistant/DaemonStatus.hs
--- a/Assistant/DaemonStatus.hs
+++ b/Assistant/DaemonStatus.hs
@@ -19,8 +19,8 @@
 import Utility.TimeStamp
 import qualified Remote
 import qualified Types.Remote as Remote
-import Config
 import Config.DynamicConfig
+import Annex.SpecialRemote.Config
 
 import Control.Concurrent.STM
 import System.Posix.Types
diff --git a/Assistant/Gpg.hs b/Assistant/Gpg.hs
--- a/Assistant/Gpg.hs
+++ b/Assistant/Gpg.hs
@@ -11,6 +11,7 @@
 import Utility.UserInfo
 import Types.Remote (RemoteConfigField)
 import Annex.SpecialRemote.Config
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 import Control.Applicative
@@ -31,7 +32,7 @@
 	deriving (Eq)
 
 {- Generates Remote configuration for encryption. -}
-configureEncryption :: EnableEncryption -> (RemoteConfigField, String)
-configureEncryption SharedEncryption = (encryptionField, "shared")
-configureEncryption NoEncryption = (encryptionField, "none")
-configureEncryption HybridEncryption = (encryptionField, "hybrid")
+configureEncryption :: EnableEncryption -> (RemoteConfigField, ProposedAccepted String)
+configureEncryption SharedEncryption = (encryptionField, Proposed "shared")
+configureEncryption NoEncryption = (encryptionField, Proposed "none")
+configureEncryption HybridEncryption = (encryptionField, Proposed "hybrid")
diff --git a/Assistant/MakeRemote.hs b/Assistant/MakeRemote.hs
--- a/Assistant/MakeRemote.hs
+++ b/Assistant/MakeRemote.hs
@@ -30,6 +30,7 @@
 import Utility.Gpg (KeyId)
 import Types.GitConfig
 import Config
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 
@@ -59,19 +60,19 @@
 	go (Just (u, c, mcu)) = setupSpecialRemote name Rsync.remote config Nothing
 		(Just u, R.Enable c, c) mcu
 	config = M.fromList
-		[ (encryptionField, "shared")
-		, ("rsyncurl", location)
-		, ("type", "rsync")
+		[ (encryptionField, Proposed "shared")
+		, (Proposed "rsyncurl", Proposed location)
+		, (typeField, Proposed "rsync")
 		]
 
 {- Inits a gcrypt special remote, and returns its name. -}
 makeGCryptRemote :: RemoteName -> String -> KeyId -> Annex RemoteName
 makeGCryptRemote remotename location keyid = 
 	initSpecialRemote remotename GCrypt.remote Nothing $ M.fromList
-		[ ("type", "gcrypt")
-		, ("gitrepo", location)
+		[ (typeField, Proposed "gcrypt")
+		, (Proposed "gitrepo", Proposed location)
 		, configureEncryption HybridEncryption
-		, ("keyid", keyid)
+		, (Proposed "keyid", Proposed keyid)
 		]
 
 type SpecialRemoteMaker = RemoteName -> RemoteType -> Maybe CredPair -> R.RemoteConfig -> Annex RemoteName
@@ -105,7 +106,7 @@
 	 - assistant, because otherwise GnuPG may block once the entropy
 	 - pool is drained, and as of now there's no way to tell the user
 	 - to perform IO actions to refill the pool. -}
-	let weakc = M.insert "highRandomQuality" "false" $ M.union config c
+	let weakc = M.insert (Proposed "highRandomQuality") (Proposed "false") (M.union config c)
 	dummycfg <- liftIO dummyRemoteGitConfig
 	(c', u) <- R.setup remotetype ss mu mcreds weakc dummycfg
 	case mcu of
diff --git a/Assistant/WebApp/Configurators/AWS.hs b/Assistant/WebApp/Configurators/AWS.hs
--- a/Assistant/WebApp/Configurators/AWS.hs
+++ b/Assistant/WebApp/Configurators/AWS.hs
@@ -25,6 +25,7 @@
 import Assistant.Gpg
 import Git.Types (RemoteName)
 import Annex.SpecialRemote.Config
+import Types.ProposedAccepted
 
 import qualified Data.Text as T
 import qualified Data.Map as M
@@ -131,10 +132,10 @@
 			let name = T.unpack $ repoName input
 			makeAWSRemote initSpecialRemote S3.remote TransferGroup (extractCreds input) name $ M.fromList
 				[ configureEncryption $ enableEncryption input
-				, ("type", "S3")
-				, ("datacenter", T.unpack $ datacenter input)
-				, ("storageclass", show $ storageClass input)
-				, ("chunk", "1MiB")
+				, (typeField, Proposed "S3")
+				, (Proposed "datacenter", Proposed $ T.unpack $ datacenter input)
+				, (Proposed "storageclass", Proposed $ show $ storageClass input)
+				, (Proposed "chunk", Proposed "1MiB")
 				]
 		_ -> $(widgetFile "configurators/adds3")
 #else
@@ -155,8 +156,8 @@
 			let name = T.unpack $ repoName input
 			makeAWSRemote initSpecialRemote Glacier.remote SmallArchiveGroup (extractCreds input) name $ M.fromList
 				[ configureEncryption $ enableEncryption input
-				, ("type", "glacier")
-				, ("datacenter", T.unpack $ datacenter input)
+				, (typeField, Proposed "glacier")
+				, (Proposed "datacenter", Proposed $ T.unpack $ datacenter input)
 				]
 		_ -> $(widgetFile "configurators/addglacier")
 #else
@@ -167,7 +168,13 @@
 #ifdef WITH_S3
 getEnableS3R uuid = do
 	m <- liftAnnex readRemoteLog
-	if maybe False S3.configIA (M.lookup uuid m)
+	isia <- case M.lookup uuid m of
+		Just c -> liftAnnex $ do
+			pc <- either mempty id . parseRemoteConfig c
+				<$> Remote.configParser S3.remote c
+			return $ S3.configIA pc
+		Nothing -> return False
+	if isia
 		then redirect $ EnableIAR uuid
 		else postEnableS3R uuid
 #else
@@ -222,7 +229,7 @@
 getRepoInfo :: RemoteConfig -> Widget
 getRepoInfo c = [whamlet|S3 remote using bucket: #{bucket}|]
   where
-	bucket = fromMaybe "" $ M.lookup "bucket" c
+	bucket = maybe "" fromProposedAccepted $ M.lookup (Accepted "bucket") c
 
 #ifdef WITH_S3
 previouslyUsedAWSCreds :: Annex (Maybe CredPair)
diff --git a/Assistant/WebApp/Configurators/Edit.hs b/Assistant/WebApp/Configurators/Edit.hs
--- a/Assistant/WebApp/Configurators/Edit.hs
+++ b/Assistant/WebApp/Configurators/Edit.hs
@@ -37,7 +37,7 @@
 import qualified Git.Config
 import qualified Annex
 import Git.Remote
-import Remote.Helper.Encryptable (extractCipher)
+import Remote.Helper.Encryptable (extractCipher, parseEncryptionConfig)
 import Types.Crypto
 import Utility.Gpg
 import Annex.UUID
@@ -46,6 +46,8 @@
 import Config.GitConfig
 import Config.DynamicConfig
 import Types.Group
+import Types.ProposedAccepted
+import Annex.SpecialRemote.Config
 
 import qualified Data.Text as T
 import qualified Data.Map as M
@@ -125,7 +127,7 @@
 				case M.lookup uuid m of
 					Nothing -> noop
 					Just remoteconfig -> configSet uuid $
-						M.insert "preferreddir" dir remoteconfig
+						M.insert (Proposed "preferreddir") (Proposed dir) remoteconfig
 	when groupChanged $ do
 		liftAnnex $ case repoGroup newc of
 			RepoGroupStandard g -> setStandardGroup uuid g
@@ -217,13 +219,21 @@
 				redirect DashboardR
 			_ -> do
 				let istransfer = repoGroup curr == RepoGroupStandard TransferGroup
-				config <- liftAnnex $ M.lookup uuid <$> readRemoteLog
+				config <- liftAnnex $ fromMaybe mempty 
+					. M.lookup uuid
+					<$> readRemoteLog
 				let repoInfo = getRepoInfo mremote config
-				let repoEncryption = getRepoEncryption mremote config
+				let repoEncryption = getRepoEncryption mremote (Just config)
 				$(widgetFile "configurators/edit/repository")
 editForm _new r@(RepoName _) = page "Edit repository" (Just Configuration) $ do
 	mr <- liftAnnex (repoIdRemote r)
-	let repoInfo = getRepoInfo mr Nothing
+	let repoInfo = case mr of
+		Just rmt -> do
+			config <- liftAnnex $ fromMaybe mempty
+				. M.lookup (Remote.uuid rmt)
+				<$> readRemoteLog
+			getRepoInfo mr config
+		Nothing -> getRepoInfo Nothing mempty
 	g <- liftAnnex gitRepo
 	mrepo <- liftAnnex $ maybe (pure Nothing) (Just <$$> Remote.getRepo) mr
 	let sshrepo = maybe False (remoteLocationIsSshUrl . flip parseRemoteLocation g . Git.repoLocation) mrepo
@@ -242,17 +252,21 @@
 			Nothing -> noop
 		_ -> noop
 
-getRepoInfo :: Maybe Remote.Remote -> Maybe Remote.RemoteConfig -> Widget
-getRepoInfo (Just r) (Just c) = case M.lookup "type" c of
-	Just "S3"
+getRepoInfo :: Maybe Remote.Remote -> Remote.RemoteConfig -> Widget
+getRepoInfo (Just r) c = case fromProposedAccepted <$> M.lookup typeField c of
+	Just "S3" -> do
 #ifdef WITH_S3
-		| S3.configIA c -> IA.getRepoInfo c
+		pc <- liftAnnex $ either mempty id . parseRemoteConfig c
+			<$> Remote.configParser S3.remote c
+		if S3.configIA pc
+			then IA.getRepoInfo c
+			else AWS.getRepoInfo c
+#else
+		AWS.getRepoInfo c
 #endif
-		| otherwise -> AWS.getRepoInfo c
 	Just t
 		| t /= "git" -> [whamlet|#{t} remote|]
 	_ -> getGitRepoInfo =<< liftAnnex (Remote.getRepo r)
-getRepoInfo (Just r) _ = getRepoInfo (Just r) (Just $ Remote.config r)
 getRepoInfo _ _ = [whamlet|git repository|]
 
 getGitRepoInfo :: Git.Repo -> Widget
@@ -261,7 +275,7 @@
 	[whamlet|git repository located at <tt>#{loc}</tt>|]
 
 getRepoEncryption :: Maybe Remote.Remote -> Maybe Remote.RemoteConfig -> Widget
-getRepoEncryption (Just _) (Just c) = case extractCipher c of
+getRepoEncryption (Just _) (Just c) = case extractCipher pc of
 	Nothing ->
 		[whamlet|not encrypted|]
 	(Just (SharedCipher _)) ->
@@ -269,6 +283,7 @@
 	(Just (EncryptedCipher _ _ ks)) -> desckeys ks
 	(Just (SharedPubKeyCipher _ ks)) -> desckeys ks
   where
+	pc = either mempty id $ parseEncryptionConfig c
 	desckeys (KeyIds { keyIds = ks }) = do
 		cmd <- liftAnnex $ gpgCmd <$> Annex.getGitConfig
 		knownkeys <- liftIO (secretKeys cmd)
diff --git a/Assistant/WebApp/Configurators/IA.hs b/Assistant/WebApp/Configurators/IA.hs
--- a/Assistant/WebApp/Configurators/IA.hs
+++ b/Assistant/WebApp/Configurators/IA.hs
@@ -25,6 +25,7 @@
 import qualified Annex.Url as Url
 import Creds
 import Annex.SpecialRemote.Config
+import Types.ProposedAccepted
 
 import qualified Data.Text as T
 import qualified Data.Map as M
@@ -131,21 +132,22 @@
 	case result of
 		FormSuccess input -> liftH $ do
 			let name = escapeBucket $ T.unpack $ itemName input
+			let wrap (k, v) = (Proposed k, Proposed v)
+			let c = map wrap $ catMaybes
+				[ Just ("type", "S3")
+				, Just ("host", S3.iaHost)
+				, Just ("bucket", escapeHeader name)
+				, Just ("x-archive-meta-title", escapeHeader $ T.unpack $ itemName input)
+				, if mediaType input == MediaOmitted
+					then Nothing
+					else Just ("x-archive-mediatype", formatMediaType $ mediaType input)
+				, (,) <$> pure "x-archive-meta-collection" <*> collectionMediaType (mediaType input)
+				-- Make item show up ASAP.
+				, Just ("x-archive-interactive-priority", "1")
+				, Just ("preferreddir", name)
+				]
 			AWS.makeAWSRemote initSpecialRemote S3.remote PublicGroup (extractCreds input) name $
-				M.fromList $ catMaybes
-					[ Just $ configureEncryption NoEncryption
-					, Just ("type", "S3")
-					, Just ("host", S3.iaHost)
-					, Just ("bucket", escapeHeader name)
-					, Just ("x-archive-meta-title", escapeHeader $ T.unpack $ itemName input)
-					, if mediaType input == MediaOmitted
-						then Nothing
-						else Just ("x-archive-mediatype", formatMediaType $ mediaType input)
-					, (,) <$> pure "x-archive-meta-collection" <*> collectionMediaType (mediaType input)
-					-- Make item show up ASAP.
-					, Just ("x-archive-interactive-priority", "1")
-					, Just ("preferreddir", name)
-					]
+				M.fromList $ configureEncryption NoEncryption : c
 		_ -> $(widgetFile "configurators/addia")
 #else
 postAddIAR = giveup "S3 not supported by this build"
@@ -202,7 +204,7 @@
     have been uploaded, and the Internet Archive has processed them.
 |]
   where
-	bucket = fromMaybe "" $ M.lookup "bucket" c
+	bucket = maybe "" fromProposedAccepted $ M.lookup (Accepted "bucket") c
 #ifdef WITH_S3
 	url = S3.iaItemUrl bucket
 #else
diff --git a/Assistant/WebApp/Configurators/Local.hs b/Assistant/WebApp/Configurators/Local.hs
--- a/Assistant/WebApp/Configurators/Local.hs
+++ b/Assistant/WebApp/Configurators/Local.hs
@@ -39,6 +39,7 @@
 import qualified Remote.GCrypt as GCrypt
 import qualified Types.Remote
 import Utility.Android
+import Types.ProposedAccepted
 
 import qualified Data.Text as T
 import qualified Data.Map as M
@@ -325,7 +326,7 @@
 		makewith $ const $ do
 			r <- liftAnnex $ addRemote $
 				enableSpecialRemote remotename' GCrypt.remote Nothing $ M.fromList
-					[("gitrepo", dir)]
+					[(Proposed "gitrepo", Proposed dir)]
 			return (u, r)
 	{- Making a new unencrypted repo, or combining with an existing one. -}
 	makeunencrypted = makewith $ \isnew -> (,)
diff --git a/Assistant/WebApp/Configurators/Ssh.hs b/Assistant/WebApp/Configurators/Ssh.hs
--- a/Assistant/WebApp/Configurators/Ssh.hs
+++ b/Assistant/WebApp/Configurators/Ssh.hs
@@ -20,6 +20,7 @@
 import Utility.UserInfo
 import Utility.Gpg
 import Types.Remote (RemoteConfig)
+import Types.ProposedAccepted
 import Git.Types (RemoteName, fromRef, fromConfigKey)
 import qualified Remote.GCrypt as GCrypt
 import qualified Annex
@@ -177,7 +178,7 @@
   where
 	enablersync sshdata u = redirect $ ConfirmSshR
 		(sshdata { sshCapabilities = [RsyncCapable] }) u
-	getsshinput = parseSshUrl <=< M.lookup "rsyncurl"
+	getsshinput = parseSshUrl . fromProposedAccepted <=< M.lookup (Accepted "rsyncurl")
 
 {- This only handles gcrypt repositories that are located on ssh servers;
  - ones on local drives are handled via another part of the UI. -}
@@ -191,7 +192,7 @@
 		sshConfigurator $
 			checkExistingGCrypt sshdata' $
 				giveup "Expected to find an encrypted git repository, but did not."
-	getsshinput = parseSshUrl <=< M.lookup "gitrepo"
+	getsshinput = parseSshUrl . fromProposedAccepted <=< M.lookup (Accepted "gitrepo")
 
 getEnableSshGitRemoteR :: UUID -> Handler Html
 getEnableSshGitRemoteR = postEnableSshGitRemoteR
@@ -200,7 +201,7 @@
   where
 	enablesshgitremote sshdata u = redirect $ ConfirmSshR sshdata u
 
-	getsshinput = parseSshUrl <=< M.lookup "location"
+	getsshinput = parseSshUrl . fromProposedAccepted <=< M.lookup (Accepted "location")
 
 {- To enable a remote that uses ssh as its transport, 
  - parse a config key to get its url, and display a form
@@ -424,7 +425,7 @@
 		$(widgetFile "configurators/ssh/combine")
 	handleexisting (Just _) = prepSsh False sshdata $ \sshdata' -> do
 		m <- liftAnnex readRemoteLog
-		case M.lookup "type" =<< M.lookup u m of
+		case fromProposedAccepted <$> (M.lookup typeField =<< M.lookup u m) of
 			Just "gcrypt" -> combineExistingGCrypt sshdata' u
 			_ -> makeSshRepo ExistingRepo sshdata'
 
@@ -474,7 +475,7 @@
 enableGCrypt sshdata reponame = setupRemote postsetup Nothing Nothing mk
   where
 	mk = enableSpecialRemote reponame GCrypt.remote Nothing $
-		M.fromList [("gitrepo", genSshUrl sshdata)]
+		M.fromList [(Proposed "gitrepo", Proposed (genSshUrl sshdata))]
 	postsetup _ = redirect DashboardR
 
 {- Combining with a gcrypt repository that may not be
@@ -546,11 +547,11 @@
 	setup r = do
 		m <- readRemoteLog
 		let c = fromMaybe M.empty (M.lookup (Remote.uuid r) m)
-		let c' = M.insert "location" (genSshUrl sshdata) $
-			M.insert "type" "git" $
-			case M.lookup nameField c of
+		let c' = M.insert (Proposed "location") (Proposed (genSshUrl sshdata)) $
+			M.insert typeField (Proposed "git") $
+			case fromProposedAccepted <$> M.lookup nameField c of
 				Just _ -> c
-				Nothing -> M.insert nameField (Remote.name r) c
+				Nothing -> M.insert nameField (Proposed (Remote.name r)) c
 		configSet (Remote.uuid r) c'
 
 makeSshRepoConnection :: RepoStatus -> Annex RemoteName -> (Remote -> Annex ()) -> Handler Html
diff --git a/Assistant/WebApp/Configurators/WebDAV.hs b/Assistant/WebApp/Configurators/WebDAV.hs
--- a/Assistant/WebApp/Configurators/WebDAV.hs
+++ b/Assistant/WebApp/Configurators/WebDAV.hs
@@ -15,13 +15,14 @@
 import qualified Remote.WebDAV as WebDAV
 import Assistant.WebApp.MakeRemote
 import qualified Remote
-import Types.Remote (RemoteConfig)
+import Types.Remote (RemoteConfig, configParser)
 import Types.StandardGroups
 import Logs.Remote
 import Git.Types (RemoteName)
 import Assistant.Gpg
 import Types.GitConfig
 import Annex.SpecialRemote.Config
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 #endif
@@ -58,10 +59,12 @@
 	m <- liftAnnex readRemoteLog
 	let c = fromJust $ M.lookup uuid m
 	let name = fromJust $ lookupName c
-	let url = fromJust $ M.lookup "url" c
+	let url = fromProposedAccepted $ fromJust $ M.lookup (Accepted "url") c
 	mcreds <- liftAnnex $ do
 		dummycfg <- liftIO dummyRemoteGitConfig
-		getRemoteCredPairFor "webdav" c dummycfg (WebDAV.davCreds uuid)
+		pc <- either mempty id . parseRemoteConfig c
+			<$> configParser WebDAV.remote c
+		getRemoteCredPairFor "webdav" pc dummycfg (WebDAV.davCreds uuid)
 	case mcreds of
 		Just creds -> webDAVConfigurator $ liftH $
 			makeWebDavRemote enableSpecialRemote name creds M.empty
diff --git a/Assistant/WebApp/RepoList.hs b/Assistant/WebApp/RepoList.hs
--- a/Assistant/WebApp/RepoList.hs
+++ b/Assistant/WebApp/RepoList.hs
@@ -26,6 +26,7 @@
 import Config.Cost
 import Utility.NotificationBroadcaster
 import qualified Git
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 import qualified Data.Set as S
@@ -175,7 +176,7 @@
 	selectedremote (Just (iscloud, _))
 		| onlyCloud reposelector = iscloud
 		| otherwise = True
-	findinfo m g u = case getconfig "type" of
+	findinfo m g u = case fromProposedAccepted <$> getconfig (Accepted "type") of
 		Just "rsync" -> val True EnableRsyncR
 		Just "directory" -> val False EnableDirectoryR
 #ifdef WITH_S3
@@ -188,12 +189,12 @@
 		Just "gcrypt" ->
 			-- Skip gcrypt repos on removable drives;
 			-- handled separately.
-			case getconfig "gitrepo" of
+			case fromProposedAccepted <$> getconfig (Accepted "gitrepo") of
 				Just rr	| remoteLocationIsUrl (parseRemoteLocation rr g) ->
 					val True EnableSshGCryptR
 				_ -> Nothing
 		Just "git" -> 
-			case getconfig "location" of
+			case fromProposedAccepted <$> getconfig (Accepted "location") of
 				Just loc | remoteLocationIsSshUrl (parseRemoteLocation loc g) ->
 					val True EnableSshGitRemoteR
 				_ -> Nothing
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,31 @@
+git-annex (7.20200202.7) upstream; urgency=medium
+
+  * add: --force-annex/--force-git options make it easier to override
+    annex.largefiles configuration (and potentially safer as it avoids
+    bugs like the smudge bug fixed in the last release).
+  * reinject --known: Fix bug that prevented it from working in a bare repo.
+  * Support being used in a git repository that uses sha256 rather than sha1.
+  * initremote, enableremote: Be stricter about rejecting invalid
+    configuration parameters for remotes, particularly things like foo=true
+    when foo=yes is expected.
+  * initremote, enableremote: Reject unknown configuration parameters
+    provided to these commands.
+  * initremote: Added --whatelse option, to show additional
+    configuration parameters you might want to set. Eg:
+    git annex initremote type=directory encryption=none --whatelse
+  * Added LISTCONFIGS to external special remote protocol. Special remote
+    programs that use GETCONFIG/SETCONFIG are recommended to implement it.
+  * init: Avoid an ugly error message when http remote has no git-annex
+    uuid configured.
+  * Support git remotes that need http basic auth to be accessed,
+    using git credential to get the password.
+  * Display a warning when concurrency is enabled but ssh connection caching
+    is not enabled or won't work due to a crippled filesystem.
+  * Makefile: Move the fish completion to the vendor_completions.d directory.
+  * Fixed a test suite failure when run in the C locale.
+
+ -- Joey Hess <id@joeyh.name>  Sun, 02 Feb 2020 00:00:00 -0400
+
 git-annex (7.20191230) upstream; urgency=medium
 
   * Optimised processing of many files, especially by commands like find
diff --git a/COPYRIGHT b/COPYRIGHT
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -2,7 +2,7 @@
 Source: native package
 
 Files: *
-Copyright: © 2010-2019 Joey Hess <id@joeyh.name>
+Copyright: © 2010-2020 Joey Hess <id@joeyh.name>
 License: AGPL-3+
 
 Files: Assistant/WebApp.hs Assistant/WebApp/* templates/* static/*
diff --git a/CmdLine/Usage.hs b/CmdLine/Usage.hs
--- a/CmdLine/Usage.hs
+++ b/CmdLine/Usage.hs
@@ -96,8 +96,8 @@
 paramItem = "ITEM"
 paramTreeish :: String
 paramTreeish = "TREEISH"
-paramKeyValue :: String
-paramKeyValue = "K=V"
+paramParamValue :: String
+paramParamValue = "PARAM=VALUE"
 paramNothing :: String
 paramNothing = ""
 paramRepeating :: String -> String
diff --git a/Command/Add.hs b/Command/Add.hs
--- a/Command/Add.hs
+++ b/Command/Add.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010-2017 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -17,8 +17,12 @@
 import Annex.FileMatcher
 import Annex.Link
 import Annex.Tmp
+import Annex.HashObject
 import Messages.Progress
+import Git.Types
 import Git.FilePath
+import qualified Git.UpdateIndex
+import Utility.FileMode
 import qualified Utility.RawFilePath as R
 
 cmd :: Command
@@ -32,6 +36,7 @@
 	, includeDotFiles :: Bool
 	, batchOption :: BatchMode
 	, updateOnly :: Bool
+	, largeFilesOverride :: Maybe Bool
 	}
 
 optParser :: CmdParamsDesc -> Parser AddOptions
@@ -47,18 +52,31 @@
 		<> short 'u'
 		<> help "only update tracked files"
 		)
+	<*> (parseforcelarge <|> parseforcesmall)
+  where
+	parseforcelarge = flag Nothing (Just True)
+		( long "force-large"
+		<> help "add all files to annex, ignoring other configuration"
+		)
+	parseforcesmall = flag Nothing (Just False)
+		( long "force-small"
+		<> help "add all files to git, ignoring other configuration"
+		)
 
 seek :: AddOptions -> CommandSeek
 seek o = startConcurrency commandStages $ do
 	largematcher <- largeFilesMatcher
 	addunlockedmatcher <- addUnlockedMatcher
-	let gofile file = ifM (checkFileMatcher largematcher (fromRawFilePath file) <||> Annex.getState Annex.force)
-		( start file addunlockedmatcher
-		, ifM (annexAddSmallFiles <$> Annex.getGitConfig)
-			( startSmall file
-			, stop
+	let gofile file = case largeFilesOverride o of
+		Nothing -> ifM (checkFileMatcher largematcher (fromRawFilePath file) <||> Annex.getState Annex.force)
+			( start file addunlockedmatcher
+			, ifM (annexAddSmallFiles <$> Annex.getGitConfig)
+				( startSmall file
+				, stop
+				)
 			)
-		)
+		Just True -> start file addunlockedmatcher
+		Just False -> startSmallOverridden file
 	case batchOption o of
 		Batch fmt
 			| updateOnly o ->
@@ -81,6 +99,29 @@
 addSmall file = do
 	showNote "non-large file; adding content to git repository"
 	addFile file
+
+startSmallOverridden :: RawFilePath -> CommandStart
+startSmallOverridden file = starting "add" (ActionItemWorkTreeFile file) $
+	next $ addSmallOverridden file
+
+addSmallOverridden :: RawFilePath -> Annex Bool
+addSmallOverridden file = do
+	showNote "adding content to git repository"
+	let file' = fromRawFilePath file
+	s <- liftIO $ getFileStatus file'
+	if isSymbolicLink s
+		then addFile file 
+		else do
+			-- Can't use addFile because the clean filter will
+			-- honor annex.largefiles and it has been overridden.
+			-- Instead, hash the file and add to the index.
+			sha <- hashFile file'
+			let ty = if isExecutable (fileMode s)
+				then TreeExecutable
+				else TreeFile
+			Annex.Queue.addUpdateIndex =<<
+				inRepo (Git.UpdateIndex.stageFile sha ty file')
+			return True
 
 addFile :: RawFilePath -> Annex Bool
 addFile file = do
diff --git a/Command/AddUrl.hs b/Command/AddUrl.hs
--- a/Command/AddUrl.hs
+++ b/Command/AddUrl.hs
@@ -261,7 +261,7 @@
 	go =<< downloadWith' downloader urlkey webUUID url (AssociatedFile (Just (toRawFilePath file)))
   where
 	urlkey = addSizeUrlKey urlinfo $ Backend.URL.fromUrl url Nothing
-	downloader f p = downloadUrl urlkey p [url] f
+	downloader f p = Url.withUrlOptions $ downloadUrl urlkey p [url] f
 	go Nothing = return Nothing
 	-- If we downloaded a html file, try to use youtube-dl to
 	-- extract embedded media.
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -24,13 +24,14 @@
 import Config
 import Config.DynamicConfig
 import Types.GitConfig
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 
 cmd :: Command
 cmd = command "enableremote" SectionSetup
 	"enables git-annex to use a remote"
-	(paramPair paramName $ paramOptional $ paramRepeating paramKeyValue)
+	(paramPair paramName $ paramOptional $ paramRepeating paramParamValue)
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
@@ -41,7 +42,7 @@
 start (name:rest) = go =<< filter matchingname <$> Annex.getGitRemotes
   where
 	matchingname r = Git.remoteName r == Just name
-	go [] = startSpecialRemote name (Logs.Remote.keyValToConfig rest)
+	go [] = startSpecialRemote name (Logs.Remote.keyValToConfig Proposed rest)
 		=<< SpecialRemote.findExisting name
 	go (r:_) = do
 		-- This could be either a normal git remote or a special
diff --git a/Command/Export.hs b/Command/Export.hs
--- a/Command/Export.hs
+++ b/Command/Export.hs
@@ -216,7 +216,7 @@
 			, (, (Nothing, Just (Git.DiffTree.file i))) <$> dstek
 			]
 	getek sha
-		| sha == nullSha = return Nothing
+		| sha `elem` nullShas = return Nothing
 		| otherwise = Just <$> exportKey sha
 
 newtype FileUploaded = FileUploaded { fromFileUploaded :: Bool }
@@ -310,7 +310,7 @@
 
 startUnexport :: Remote -> ExportHandle -> TopFilePath -> [Git.Sha] -> CommandStart
 startUnexport r db f shas = do
-	eks <- forM (filter (/= nullSha) shas) exportKey
+	eks <- forM (filter (`notElem` nullShas) shas) exportKey
 	if null eks
 		then stop
 		else starting ("unexport " ++ name r) (ActionItemOther (Just (fromRawFilePath f'))) $
@@ -359,7 +359,7 @@
 
 startRecoverIncomplete :: Remote -> ExportHandle -> Git.Sha -> TopFilePath -> CommandStart
 startRecoverIncomplete r db sha oldf
-	| sha == nullSha = stop
+	| sha `elem` nullShas = stop
 	| otherwise = do
 		ek <- exportKey sha
 		let loc = exportTempName ek
diff --git a/Command/InitRemote.hs b/Command/InitRemote.hs
--- a/Command/InitRemote.hs
+++ b/Command/InitRemote.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -16,27 +16,35 @@
 import qualified Remote
 import qualified Logs.Remote
 import qualified Types.Remote as R
+import Types.RemoteConfig
 import Annex.UUID
 import Logs.UUID
 import Logs.Remote
 import Types.GitConfig
+import Types.ProposedAccepted
 import Config
 
 cmd :: Command
 cmd = command "initremote" SectionSetup
 	"creates a special (non-git) remote"
-	(paramPair paramName $ paramOptional $ paramRepeating paramKeyValue)
+	(paramPair paramName $ paramOptional $ paramRepeating paramParamValue)
 	(seek <$$> optParser)
 
 data InitRemoteOptions = InitRemoteOptions
 	{ cmdparams :: CmdParams
 	, sameas :: Maybe (DeferredParse UUID)
+	, whatElse :: Bool
 	}
 
 optParser :: CmdParamsDesc -> Parser InitRemoteOptions
 optParser desc = InitRemoteOptions
 	<$> cmdParams desc
 	<*> optional parseSameasOption
+	<*> switch
+		( long "whatelse"
+		<> short 'w'
+		<> help "describe other configuration parameters for a special remote"
+		)
 
 parseSameasOption :: Parser (DeferredParse UUID)
 parseSameasOption = parseUUIDOption <$> strOption
@@ -63,27 +71,34 @@
 					(Just . Sameas <$$> getParsed)
 					(sameas o) 
 				c <- newConfig name sameasuuid
-					(Logs.Remote.keyValToConfig ws)
+					(Logs.Remote.keyValToConfig Proposed ws)
 					<$> readRemoteLog
 				t <- either giveup return (findType c)
-				starting "initremote" (ActionItemOther (Just name)) $
-					perform t name c o
+				if whatElse o
+					then startingCustomOutput (ActionItemOther Nothing) $
+						describeOtherParamsFor c t
+					else starting "initremote" (ActionItemOther (Just name)) $
+						perform t name c o
 			)
 	)
 
 perform :: RemoteType -> String -> R.RemoteConfig -> InitRemoteOptions -> CommandPerform
 perform t name c o = do
 	dummycfg <- liftIO dummyRemoteGitConfig
-	(c', u) <- R.setup t R.Init (sameasu <|> uuidfromuser) Nothing c dummycfg
-	next $ cleanup u name c' o
+	let c' = M.delete uuidField c
+	(c'', u) <- R.setup t R.Init (sameasu <|> uuidfromuser) Nothing c' dummycfg
+	next $ cleanup u name c'' o
   where
-	uuidfromuser = case M.lookup "uuid" c of
+	uuidfromuser = case fromProposedAccepted <$> M.lookup uuidField c of
 		Just s
 			| isUUID s -> Just (toUUID s)
 			| otherwise -> giveup "invalid uuid"
 		Nothing -> Nothing
-	sameasu = toUUID <$> M.lookup sameasUUIDField c
+	sameasu = toUUID . fromProposedAccepted <$> M.lookup sameasUUIDField c
 
+uuidField :: R.RemoteConfigField
+uuidField = Accepted "uuid"
+
 cleanup :: UUID -> String -> R.RemoteConfig -> InitRemoteOptions -> CommandCleanup
 cleanup u name c o = do
 	case sameas o of
@@ -95,3 +110,26 @@
 			setConfig (remoteConfig c "config-uuid") (fromUUID cu)
 			Logs.Remote.configSet cu c
 	return True
+
+describeOtherParamsFor :: RemoteConfig -> RemoteType -> CommandPerform
+describeOtherParamsFor c t = do
+	cp <- R.configParser t c
+	let l = map mk (filter notinconfig $ remoteConfigFieldParsers cp)
+		++ map mk' (maybe [] snd (remoteConfigRestPassthrough cp))
+	liftIO $ forM_ l $ \(p, fd, vd) -> case fd of
+		HiddenField -> return ()
+		FieldDesc d -> do
+			putStrLn p
+			putStrLn ("\t" ++ d)
+			case vd of
+				Nothing -> return ()
+				Just (ValueDesc d') ->
+					putStrLn $ "\t(" ++ d' ++ ")"
+	next $ return True
+  where
+	notinconfig fp = not (M.member (parserForField fp) c)
+	mk fp = ( fromProposedAccepted (parserForField fp)
+		, fieldDesc fp
+		, valueDesc fp
+		)
+	mk' (k, v) = (k, v, Nothing)
diff --git a/Command/Log.hs b/Command/Log.hs
--- a/Command/Log.hs
+++ b/Command/Log.hs
@@ -215,7 +215,7 @@
 		, Param "-z"
 		, Param "--pretty=format:%ct"
 		, Param "--raw"
-		, Param "--abbrev=40"
+		, Param "--no-abbrev"
 		] ++ os ++
 		[ Param $ Git.fromRef Annex.Branch.fullname
 		, Param "--"
diff --git a/Command/Reinject.hs b/Command/Reinject.hs
--- a/Command/Reinject.hs
+++ b/Command/Reinject.hs
@@ -13,6 +13,7 @@
 import Backend
 import Types.KeySource
 import Utility.Metered
+import qualified Git
 
 cmd :: Command
 cmd = command "reinject" SectionUtility 
@@ -65,8 +66,13 @@
 				)
 
 notAnnexed :: FilePath -> CommandStart -> CommandStart
-notAnnexed src = ifAnnexed (toRawFilePath src) $
-	giveup $ "cannot used annexed file as src: " ++ src
+notAnnexed src a = 
+	ifM (fromRepo Git.repoIsLocalBare)
+		( a
+		, ifAnnexed (toRawFilePath src)
+			(giveup $ "cannot used annexed file as src: " ++ src)
+			a
+		)
 
 perform :: FilePath -> Key -> CommandPerform
 perform src key = ifM move
diff --git a/Command/RenameRemote.hs b/Command/RenameRemote.hs
--- a/Command/RenameRemote.hs
+++ b/Command/RenameRemote.hs
@@ -13,6 +13,7 @@
 import qualified Logs.Remote
 import qualified Types.Remote as R
 import qualified Remote
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 
@@ -50,6 +51,6 @@
 	let (namefield, cu) = case mcu of
 		Nothing -> (nameField, u)
 		Just (Annex.SpecialRemote.ConfigFrom u') -> (sameasNameField, u')
-	Logs.Remote.configSet cu (M.insert namefield newname cfg)
+	Logs.Remote.configSet cu (M.insert namefield (Proposed newname) cfg)
 	
 	next $ return True
diff --git a/Command/Sync.hs b/Command/Sync.hs
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -43,6 +43,7 @@
 import qualified Remote.Git
 import Config
 import Config.GitConfig
+import Annex.SpecialRemote.Config
 import Config.DynamicConfig
 import Config.Files
 import Annex.Wanted
diff --git a/Command/TestRemote.hs b/Command/TestRemote.hs
--- a/Command/TestRemote.hs
+++ b/Command/TestRemote.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2014-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2014-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -24,8 +24,12 @@
 import Utility.CopyFile
 import Types.Messages
 import Types.Export
+import Types.Crypto
+import Types.RemoteConfig
+import Annex.SpecialRemote.Config (exportTreeField)
 import Remote.Helper.ExportImport
 import Remote.Helper.Chunked
+import Remote.Helper.Encryptable (describeEncryption, encryptionField, highRandomQualityField)
 import Git.Types
 
 import Test.Tasty
@@ -109,7 +113,7 @@
 	desc r' k = intercalate "; " $ map unwords
 		[ [ "key size", show (fromKey keySize k) ]
 		, [ show (getChunkConfig (Remote.config r')) ]
-		, ["encryption", fromMaybe "none" (M.lookup "encryption" (Remote.config r'))]
+		, ["encryption", describeEncryption (Remote.config r')]
 		]
 	descexport k1 k2 = intercalate "; " $ map unwords
 		[ [ "exporttree=yes" ]
@@ -119,33 +123,35 @@
 
 adjustChunkSize :: Remote -> Int -> Annex (Maybe Remote)
 adjustChunkSize r chunksize = adjustRemoteConfig r
-	(M.insert "chunk" (show chunksize))
+	(M.insert chunkField (RemoteConfigValue (show chunksize)))
 
 -- Variants of a remote with no encryption, and with simple shared
 -- encryption. Gpg key based encryption is not tested.
 encryptionVariants :: Remote -> Annex [Remote]
 encryptionVariants r = do
-	noenc <- adjustRemoteConfig r (M.insert "encryption" "none")
+	noenc <- adjustRemoteConfig r $
+		M.insert encryptionField (RemoteConfigValue NoneEncryption)
 	sharedenc <- adjustRemoteConfig r $
-		M.insert "encryption" "shared" .
-		M.insert "highRandomQuality" "false"
+		M.insert encryptionField (RemoteConfigValue SharedEncryption) .
+		M.insert highRandomQualityField (RemoteConfigValue False)
 	return $ catMaybes [noenc, sharedenc]
 
 -- Variant of a remote with exporttree disabled.
 disableExportTree :: Remote -> Annex Remote
 disableExportTree r = maybe (error "failed disabling exportree") return 
-		=<< adjustRemoteConfig r (M.delete "exporttree")
+		=<< adjustRemoteConfig r (M.delete exportTreeField)
 
 -- Variant of a remote with exporttree enabled.
 exportTreeVariant :: Remote -> Annex (Maybe Remote)
 exportTreeVariant r = ifM (Remote.isExportSupported r)
 	( adjustRemoteConfig r $
-		M.insert "encryption" "none" . M.insert "exporttree" "yes"
+		M.insert encryptionField (RemoteConfigValue NoneEncryption) . 
+		M.insert exportTreeField (RemoteConfigValue True)
 	, return Nothing
 	)
 
 -- Regenerate a remote with a modified config.
-adjustRemoteConfig :: Remote -> (Remote.RemoteConfig -> Remote.RemoteConfig) -> Annex (Maybe Remote)
+adjustRemoteConfig :: Remote -> (Remote.ParsedRemoteConfig -> Remote.ParsedRemoteConfig) -> Annex (Maybe Remote)
 adjustRemoteConfig r adjustconfig = do
 	repo <- Remote.getRepo r
 	Remote.generate (Remote.remotetype r)
diff --git a/Command/Undo.hs b/Command/Undo.hs
--- a/Command/Undo.hs
+++ b/Command/Undo.hs
@@ -58,7 +58,7 @@
 	-- Take two passes through the diff, first doing any removals,
 	-- and then any adds. This order is necessary to handle eg, removing
 	-- a directory and replacing it with a file.
-	let (removals, adds) = partition (\di -> dstsha di == nullSha) diff'
+	let (removals, adds) = partition (\di -> dstsha di `elem` nullShas) diff'
 	let mkrel di = liftIO $ relPathCwdToFile $ fromRawFilePath $
 		fromTopFilePath (file di) g
 
diff --git a/Command/Unused.hs b/Command/Unused.hs
--- a/Command/Unused.hs
+++ b/Command/Unused.hs
@@ -267,7 +267,7 @@
   where
   	go d = do
 		let sha = extractsha d
-		unless (sha == nullSha) $
+		unless (sha `elem` nullShas) $
 			catKey sha >>= maybe noop a
 
 {- Filters out keys that have an associated file that's not modified. -}
diff --git a/Config.hs b/Config.hs
--- a/Config.hs
+++ b/Config.hs
@@ -20,9 +20,7 @@
 import Types.Availability
 import Git.Types
 import qualified Types.Remote as Remote
-import qualified Annex.SpecialRemote.Config as SpecialRemote
 
-import qualified Data.Map as M
 import qualified Data.ByteString as S
 
 type UnqualifiedConfigKey = S.ByteString
@@ -64,9 +62,6 @@
 instance RemoteNameable Remote where
 	getRemoteName = Remote.name
 
-instance RemoteNameable Remote.RemoteConfig where
-	getRemoteName c = fromMaybe "" (SpecialRemote.lookupName c)
-
 {- A per-remote config setting in git config. -}
 remoteConfig :: RemoteNameable r => r -> UnqualifiedConfigKey -> ConfigKey
 remoteConfig r key = ConfigKey $
@@ -96,12 +91,6 @@
 
 setRemoteBare :: Git.Repo -> Bool -> Annex ()
 setRemoteBare r b = setConfig (remoteConfig r "bare") (Git.Config.boolConfig b)
-
-exportTree :: Remote.RemoteConfig -> Bool
-exportTree c = fromMaybe False $ yesNo =<< M.lookup "exporttree" c
-
-importTree :: Remote.RemoteConfig -> Bool
-importTree c = fromMaybe False $ yesNo =<< M.lookup "importtree" c
 
 isBareRepo :: Annex Bool
 isBareRepo = fromRepo Git.repoIsLocalBare
diff --git a/Creds.hs b/Creds.hs
--- a/Creds.hs
+++ b/Creds.hs
@@ -1,6 +1,6 @@
 {- Credentials storage
  -
- - Copyright 2012-2014 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -9,6 +9,7 @@
 	module Types.Creds,
 	CredPairStorage(..),
 	setRemoteCredPair,
+	setRemoteCredPair',
 	getRemoteCredPair,
 	getRemoteCredPairFor,
 	missingCredPairFor,
@@ -23,11 +24,14 @@
 import Annex.Common
 import qualified Annex
 import Types.Creds
+import Types.RemoteConfig
+import Annex.SpecialRemote.Config
 import Annex.Perms
 import Utility.FileMode
 import Crypto
 import Types.Remote (RemoteConfig, RemoteConfigField)
-import Remote.Helper.Encryptable (remoteCipher, remoteCipher', embedCreds, EncryptionIsSetup, extractCipher)
+import Types.ProposedAccepted
+import Remote.Helper.Encryptable (remoteCipher, remoteCipher', embedCreds, EncryptionIsSetup, extractCipher, parseEncryptionConfig)
 import Utility.Env (getEnv)
 
 import qualified Data.ByteString.Lazy.Char8 as L
@@ -53,32 +57,47 @@
  - cipher. The EncryptionIsSetup is witness to that being the case.
  -}
 setRemoteCredPair :: EncryptionIsSetup -> RemoteConfig -> RemoteGitConfig -> CredPairStorage -> Maybe CredPair -> Annex RemoteConfig
-setRemoteCredPair encsetup c gc storage mcreds = case mcreds of
-	Nothing -> maybe (return c) (setRemoteCredPair encsetup c gc storage . Just)
-		=<< getRemoteCredPair c gc storage
+setRemoteCredPair = setRemoteCredPair' id
+	(either (const mempty) id . parseEncryptionConfig)
+
+setRemoteCredPair'
+	:: (ProposedAccepted String -> a)
+	-> (M.Map RemoteConfigField a -> ParsedRemoteConfig)
+	-> EncryptionIsSetup
+	-> M.Map RemoteConfigField a
+	-> RemoteGitConfig
+	-> CredPairStorage
+	-> Maybe CredPair
+	-> Annex (M.Map RemoteConfigField a)
+setRemoteCredPair' mkval parseconfig encsetup c gc storage mcreds = case mcreds of
+	Nothing -> maybe (return c) (setRemoteCredPair' mkval parseconfig encsetup c gc storage . Just)
+		=<< getRemoteCredPair pc gc storage
 	Just creds
-		| embedCreds c ->
+		| embedCreds pc -> do
 			let key = credPairRemoteField storage
-			in storeconfig creds key =<< flip remoteCipher gc =<< localcache creds
-		| otherwise -> localcache creds
+			localcache creds
+			storeconfig creds key =<< remoteCipher pc gc
+		| otherwise -> do
+			localcache creds
+			return c
   where
-	localcache creds = do
-		writeCacheCredPair creds storage
-		return c
+	localcache creds = writeCacheCredPair creds storage
 
 	storeconfig creds key (Just cipher) = do
 		cmd <- gpgCmd <$> Annex.getGitConfig
-		s <- liftIO $ encrypt cmd (c, gc) cipher
+		s <- liftIO $ encrypt cmd (pc, gc) cipher
 			(feedBytes $ L.pack $ encodeCredPair creds)
 			(readBytes $ return . L.unpack)
-		return $ M.insert key (toB64 s) c
+		return $ M.insert key (mkval (Accepted (toB64 s))) c
 	storeconfig creds key Nothing =
-		return $ M.insert key (toB64 $ encodeCredPair creds) c
+		return $ M.insert key (mkval (Accepted (toB64 $ encodeCredPair creds))) c
+	
+	pc = parseconfig c
 
 {- Gets a remote's credpair, from the environment if set, otherwise
  - from the cache in gitAnnexCredsDir, or failing that, from the
  - value in RemoteConfig. -}
-getRemoteCredPair :: RemoteConfig -> RemoteGitConfig -> CredPairStorage -> Annex (Maybe CredPair)
+getRemoteCredPair :: ParsedRemoteConfig -> RemoteGitConfig -> CredPairStorage -> Annex (Maybe CredPair)
 getRemoteCredPair c gc storage = maybe fromcache (return . Just) =<< fromenv
   where
 	fromenv = liftIO $ getEnvCredPair storage
@@ -86,7 +105,7 @@
 	fromconfig = do
 		let key = credPairRemoteField storage
 		mcipher <- remoteCipher' c gc
-		case (M.lookup key c, mcipher) of
+		case (fromProposedAccepted <$> getRemoteConfigValue key c, mcipher) of
 			(Nothing, _) -> return Nothing
 			(Just enccreds, Just (cipher, storablecipher)) ->
 				fromenccreds enccreds cipher storablecipher
@@ -114,7 +133,7 @@
 			return $ Just credpair
 		_ -> error "bad creds"
 
-getRemoteCredPairFor :: String -> RemoteConfig -> RemoteGitConfig -> CredPairStorage -> Annex (Maybe CredPair)
+getRemoteCredPairFor :: String -> ParsedRemoteConfig -> RemoteGitConfig -> CredPairStorage -> Annex (Maybe CredPair)
 getRemoteCredPairFor this c gc storage = go =<< getRemoteCredPair c gc storage
   where
 	go Nothing = do
@@ -183,7 +202,7 @@
 	let f = d </> file
 	liftIO $ nukeFile f
 
-includeCredsInfo :: RemoteConfig -> CredPairStorage -> [(String, String)] -> Annex [(String, String)]
+includeCredsInfo :: ParsedRemoteConfig -> CredPairStorage -> [(String, String)] -> Annex [(String, String)]
 includeCredsInfo c storage info = do
 	v <- liftIO $ getEnvCredPair storage
 	case v of
diff --git a/Crypto.hs b/Crypto.hs
--- a/Crypto.hs
+++ b/Crypto.hs
@@ -3,7 +3,7 @@
  - Currently using gpg; could later be modified to support different
  - crypto backends if neccessary.
  -
- - Copyright 2011-2016 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -13,6 +13,7 @@
 {-# LANGUAGE Rank2Types #-}
 
 module Crypto (
+	EncryptionMethod(..),
 	Cipher,
 	KeyIds(..),
 	EncKey,
@@ -37,7 +38,6 @@
 import qualified Data.ByteString as S
 import qualified Data.ByteString.Lazy as L
 import Data.ByteString.UTF8 (fromString)
-import qualified Data.Map as M
 import Control.Monad.IO.Class
 
 import Annex.Common
@@ -232,14 +232,18 @@
 
 {- Extract the GnuPG options from a pair of a Remote Config and a Remote
  - Git Config. -}
-instance LensGpgEncParams (RemoteConfig, RemoteGitConfig) where
+instance LensGpgEncParams (ParsedRemoteConfig, RemoteGitConfig) where
 	getGpgEncParamsBase (_c,gc) = map Param (remoteAnnexGnupgOptions gc)
 	getGpgEncParams (c,gc) = getGpgEncParamsBase (c,gc) ++
  		{- When the remote is configured to use public-key encryption,
 		 - look up the recipient keys and add them to the option list. -}
-		case M.lookup encryptionField c of
-			Just "pubkey" -> Gpg.pkEncTo $ maybe [] (splitc ',') $ M.lookup cipherkeysField c
-			Just "sharedpubkey" -> Gpg.pkEncTo $ maybe [] (splitc ',') $ M.lookup pubkeysField c
+		case getRemoteConfigValue encryptionField c of
+			Just PubKeyEncryption -> 
+				Gpg.pkEncTo $ maybe [] (splitc ',') $
+					getRemoteConfigValue cipherkeysField c
+			Just SharedPubKeyEncryption ->
+				Gpg.pkEncTo $ maybe [] (splitc ',') $
+					getRemoteConfigValue pubkeysField c
 			_ -> []
 	getGpgDecParams (_c,gc) = map Param (remoteAnnexGnupgDecryptOptions gc)
 
diff --git a/Database/Export.hs b/Database/Export.hs
--- a/Database/Export.hs
+++ b/Database/Export.hs
@@ -233,7 +233,7 @@
 	void $ liftIO cleanup
   where
 	getek sha
-		| sha == nullSha = return Nothing
+		| sha `elem` nullShas = return Nothing
 		| otherwise = Just <$> exportKey sha
 
 {- Diff from the old to the new tree and update the ExportTree table. -}
diff --git a/Database/Keys.hs b/Database/Keys.hs
--- a/Database/Keys.hs
+++ b/Database/Keys.hs
@@ -260,7 +260,7 @@
 		, Param "--cached"
 		, Param "--raw"
 		, Param "-z"
-		, Param "--abbrev=40"
+		, Param "--no-abbrev"
 		-- Optimization: Only find pointer files. This is not
 		-- perfect. A file could start with this and not be a
 		-- pointer file. And a pointer file that is replaced with
diff --git a/Git/CatFile.hs b/Git/CatFile.hs
--- a/Git/CatFile.hs
+++ b/Git/CatFile.hs
@@ -148,13 +148,12 @@
 	| " missing" `isSuffixOf` l -- less expensive than full check
 		&& l == fromRef object ++ " missing" = Just DNE
 	| otherwise = case words l of
-		[sha, objtype, size]
-			| length sha == shaSize ->
-				case (readObjectType (encodeBS objtype), reads size) of
-					(Just t, [(bytes, "")]) -> 
-						Just $ ParsedResp (Ref sha) bytes t
-					_ -> Nothing
-			| otherwise -> Nothing
+		[sha, objtype, size] -> case extractSha sha of
+			Just sha' -> case (readObjectType (encodeBS objtype), reads size) of
+				(Just t, [(bytes, "")]) -> 
+					Just $ ParsedResp sha' bytes t
+				_ -> Nothing
+			Nothing -> Nothing
 		_ -> Nothing
 
 querySingle :: CommandParam -> Ref -> Repo -> (Handle -> IO a) -> IO (Maybe a)
diff --git a/Git/Config.hs b/Git/Config.hs
--- a/Git/Config.hs
+++ b/Git/Config.hs
@@ -1,6 +1,6 @@
 {- git repository configuration handling
  -
- - Copyright 2010-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -14,6 +14,7 @@
 import qualified Data.ByteString.Char8 as S8
 import Data.Char
 import qualified System.FilePath.ByteString as P
+import Control.Concurrent.Async
 
 import Common
 import Git
@@ -184,19 +185,22 @@
 
 {- Runs a command to get the configuration of a repo,
  - and returns a repo populated with the configuration, as well as the raw
- - output of the command. -}
-fromPipe :: Repo -> String -> [CommandParam] -> IO (Either SomeException (Repo, S.ByteString))
+ - output and any standard output of the command. -}
+fromPipe :: Repo -> String -> [CommandParam] -> IO (Either SomeException (Repo, S.ByteString, S.ByteString))
 fromPipe r cmd params = try $
-	withHandle StdoutHandle createProcessSuccess p $ \h -> do
-		val <- S.hGetContents h
+	withOEHandles createProcessSuccess p $ \(hout, herr) -> do
+		geterr <- async $ S.hGetContents herr
+		getval <- async $ S.hGetContents hout
+		val <- wait getval
+		err <- wait geterr
 		r' <- store val r
-		return (r', val)
+		return (r', val, err)
   where
 	p = proc cmd $ toCommand params
 
 {- Reads git config from a specified file and returns the repo populated
  - with the configuration. -}
-fromFile :: Repo -> FilePath -> IO (Either SomeException (Repo, S.ByteString))
+fromFile :: Repo -> FilePath -> IO (Either SomeException (Repo, S.ByteString, S.ByteString))
 fromFile r f = fromPipe r "git"
 	[ Param "config"
 	, Param "--file"
diff --git a/Git/Credential.hs b/Git/Credential.hs
--- a/Git/Credential.hs
+++ b/Git/Credential.hs
@@ -1,6 +1,6 @@
 {- git credential interface
  -
- - Copyright 2019 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -21,6 +21,23 @@
 
 credentialPassword :: Credential -> Maybe String
 credentialPassword = M.lookup "password" . fromCredential
+
+credentialBasicAuth :: Credential -> Maybe BasicAuth
+credentialBasicAuth cred = BasicAuth
+	<$> credentialUsername cred
+	<*> credentialPassword cred
+
+getBasicAuthFromCredential :: Repo -> GetBasicAuth
+getBasicAuthFromCredential r u = do
+	c <- getUrlCredential u r
+	case credentialBasicAuth c of
+		Just ba -> return $ Just (ba, signalsuccess c)
+		Nothing -> do
+			signalsuccess c False
+			return Nothing
+  where
+	signalsuccess c True = approveUrlCredential c r
+	signalsuccess c False = rejectUrlCredential c r
 
 -- | This may prompt the user for login information, or get cached login
 -- information.
diff --git a/Git/DiffTree.hs b/Git/DiffTree.hs
--- a/Git/DiffTree.hs
+++ b/Git/DiffTree.hs
@@ -77,14 +77,14 @@
  - is adjusted to be the same as diff-tree --raw._-}
 diffLog :: [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool)
 diffLog params = getdiff (Param "log")
-	(Param "-n1" : Param "--abbrev=40" : Param "--pretty=format:" : params)
+	(Param "-n1" : Param "--no-abbrev" : Param "--pretty=format:" : params)
 
 {- Uses git show to get the changes made by a commit.
  -
  - Does not support merge commits, and will fail on them. -}
 commitDiff :: Sha -> Repo -> IO ([DiffTreeItem], IO Bool)
 commitDiff ref = getdiff (Param "show")
-	[ Param "--abbrev=40", Param "--pretty=", Param "--raw", Param (fromRef ref) ]
+	[ Param "--no-abbrev", Param "--pretty=", Param "--raw", Param (fromRef ref) ]
 
 getdiff :: CommandParam -> [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool)
 getdiff command params repo = do
@@ -119,10 +119,7 @@
 		readmode = fst . Prelude.head . readOct
 
 		-- info = :<srcmode> SP <dstmode> SP <srcsha> SP <dstsha> SP <status>
-		-- All fields are fixed, so we can pull them out of
-		-- specific positions in the line.
 		(srcm, past_srcm) = splitAt 7 $ drop 1 info
 		(dstm, past_dstm) = splitAt 7 past_srcm
-		(ssha, past_ssha) = splitAt shaSize past_dstm
-		(dsha, past_dsha) = splitAt shaSize $ drop 1 past_ssha
-		s = drop 1 past_dsha
+		(ssha, past_ssha) = separate (== ' ') past_dstm
+		(dsha, s) = separate (== ' ') past_ssha
diff --git a/Git/DiffTreeItem.hs b/Git/DiffTreeItem.hs
--- a/Git/DiffTreeItem.hs
+++ b/Git/DiffTreeItem.hs
@@ -17,8 +17,8 @@
 data DiffTreeItem = DiffTreeItem
 	{ srcmode :: FileMode
 	, dstmode :: FileMode
-	, srcsha :: Sha -- nullSha if file was added
-	, dstsha :: Sha -- nullSha if file was deleted
+	, srcsha :: Sha -- null sha if file was added
+	, dstsha :: Sha -- null sha if file was deleted
 	, status :: String
 	, file :: TopFilePath
 	} deriving Show
diff --git a/Git/LsFiles.hs b/Git/LsFiles.hs
--- a/Git/LsFiles.hs
+++ b/Git/LsFiles.hs
@@ -158,17 +158,20 @@
 stagedDetails' :: [CommandParam] -> [RawFilePath] -> Repo -> IO ([StagedDetails], IO Bool)
 stagedDetails' ps l repo = do
 	(ls, cleanup) <- pipeNullSplit params repo
-	return (map parse ls, cleanup)
+	return (map parseStagedDetails ls, cleanup)
   where
 	params = Param "ls-files" : Param "--stage" : Param "-z" : ps ++ 
 		Param "--" : map (File . fromRawFilePath) l
-	parse s
-		| null file = (L.toStrict s, Nothing, Nothing)
-		| otherwise = (toRawFilePath file, extractSha $ take shaSize rest, readmode mode)
-	  where
-		(metadata, file) = separate (== '\t') (decodeBL' s)
-		(mode, rest) = separate (== ' ') metadata
-		readmode = fst <$$> headMaybe . readOct
+
+parseStagedDetails :: L.ByteString -> StagedDetails
+parseStagedDetails s
+	| null file = (L.toStrict s, Nothing, Nothing)
+	| otherwise = (toRawFilePath file, extractSha sha, readmode mode)
+  where
+	(metadata, file) = separate (== '\t') (decodeBL' s)
+	(mode, metadata') = separate (== ' ') metadata
+	(sha, _) = separate (== ' ') metadata'
+	readmode = fst <$$> headMaybe . readOct
 
 {- Returns a list of the files in the specified locations that are staged
  - for commit, and whose type has changed. -}
diff --git a/Git/LsTree.hs b/Git/LsTree.hs
--- a/Git/LsTree.hs
+++ b/Git/LsTree.hs
@@ -94,10 +94,10 @@
 	<$> octal
 	<* A8.char ' '
 	-- type
-	<*> A.takeTill (== 32)
+	<*> A8.takeTill (== ' ')
 	<* A8.char ' '
 	-- sha
-	<*> (Ref . decodeBS' <$> A.take shaSize)
+	<*> (Ref . decodeBS' <$> A8.takeTill (== '\t'))
 	<* A8.char '\t'
 	-- file
 	<*> (asTopFilePath . Git.Filename.decode <$> A.takeByteString)
diff --git a/Git/Remote.hs b/Git/Remote.hs
--- a/Git/Remote.hs
+++ b/Git/Remote.hs
@@ -117,5 +117,5 @@
 	-- git on Windows will write a path to .git/config with "drive:",
 	-- which is not to be confused with a "host:"
 	dosstyle = hasDrive
-	dospath = fromInternalGitPath
+	dospath = fromRawFilePath . fromInternalGitPath . toRawFilePath
 #endif
diff --git a/Git/Sha.hs b/Git/Sha.hs
--- a/Git/Sha.hs
+++ b/Git/Sha.hs
@@ -1,6 +1,6 @@
 {- git SHA stuff
  -
- - Copyright 2011 Joey Hess <id@joeyh.name>
+ - Copyright 2011,2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -21,8 +21,8 @@
  - it, but nothing else. -}
 extractSha :: String -> Maybe Sha
 extractSha s
-	| len == shaSize = val s
-	| len == shaSize + 1 && length s' == shaSize = val s'
+	| len `elem` shaSizes = val s
+	| len - 1 `elem` shaSizes && length s' == len - 1 = val s'
 	| otherwise = Nothing
   where
 	len = length s
@@ -31,13 +31,30 @@
 		| all (`elem` "1234567890ABCDEFabcdef") v = Just $ Ref v
 		| otherwise = Nothing
 
-{- Size of a git sha. -}
-shaSize :: Int
-shaSize = 40
+{- Sizes of git shas. -}
+shaSizes :: [Int]
+shaSizes = 
+	[ 40 -- sha1 (must come first)
+	, 64 -- sha256
+	]
 
-nullSha :: Ref		
-nullSha = Ref $ replicate shaSize '0'
+{- Git plumbing often uses a all 0 sha to represent things like a
+ - deleted file. -}
+nullShas :: [Sha]
+nullShas = map (\n -> Ref (replicate n '0')) shaSizes
 
-{- Git's magic empty tree. -}
+{- Sha to provide to git plumbing when deleting a file.
+ -
+ - It's ok to provide a sha1; git versions that use sha256 will map the
+ - sha1 to the sha256, or probably just treat all null sha1 specially
+ - the same as all null sha256. -}
+deleteSha :: Sha
+deleteSha = Prelude.head nullShas
+
+{- Git's magic empty tree.
+ -
+ - It's ok to provide the sha1 of this to git to refer to an empty tree;
+ - git versions that use sha256 will map the sha1 to the sha256.
+ -}
 emptyTree :: Ref
 emptyTree = Ref "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
diff --git a/Git/UnionMerge.hs b/Git/UnionMerge.hs
--- a/Git/UnionMerge.hs
+++ b/Git/UnionMerge.hs
@@ -82,7 +82,7 @@
  - a line suitable for update-index that union merges the two sides of the
  - diff. -}
 mergeFile :: String -> RawFilePath -> HashObjectHandle -> CatFileHandle -> IO (Maybe L.ByteString)
-mergeFile info file hashhandle h = case filter (/= nullSha) [Ref asha, Ref bsha] of
+mergeFile info file hashhandle h = case filter (`notElem` nullShas) [Ref asha, Ref bsha] of
 	[] -> return Nothing
 	(sha:[]) -> use sha
 	shas -> use
diff --git a/Git/UpdateIndex.hs b/Git/UpdateIndex.hs
--- a/Git/UpdateIndex.hs
+++ b/Git/UpdateIndex.hs
@@ -108,7 +108,7 @@
 unstageFile' :: TopFilePath -> Streamer
 unstageFile' p = pureStreamer $ L.fromStrict $
 	"0 "
-	<> encodeBS' (fromRef nullSha)
+	<> encodeBS' (fromRef deleteSha)
 	<> "\t"
 	<> indexPath p
 
diff --git a/Key.hs b/Key.hs
--- a/Key.hs
+++ b/Key.hs
@@ -1,6 +1,6 @@
 {- git-annex Keys
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -28,6 +28,7 @@
 	prop_isomorphic_key_encode
 ) where
 
+import Data.Char
 import qualified Data.Text as T
 import qualified Data.ByteString as S
 import qualified Data.Attoparsec.ByteString as A
@@ -79,11 +80,15 @@
 		<*> ((succ . abs <$>) <$> arbitrary) -- chunknum cannot be 0 or negative
 
 -- AssociatedFile cannot be empty, and cannot contain a NUL
--- (but can be Nothing)
+-- (but can be Nothing). 
 instance Arbitrary AssociatedFile where
-	arbitrary = (AssociatedFile . fmap toRawFilePath <$> arbitrary)
+  	arbitrary = (AssociatedFile . fmap conv <$> arbitrary)
 		`suchThat` (/= AssociatedFile (Just S.empty))
 		`suchThat` (\(AssociatedFile f) -> maybe True (S.notElem 0) f)
+	  where
+		-- Generating arbitrary unicode leads to encoding errors
+		-- when LANG=C, so limit to ascii.
+		conv = toRawFilePath . filter isAscii
 
 instance Arbitrary Key where
 	arbitrary = mkKey . const <$> arbitrary
diff --git a/Logs/Remote/Pure.hs b/Logs/Remote/Pure.hs
--- a/Logs/Remote/Pure.hs
+++ b/Logs/Remote/Pure.hs
@@ -19,6 +19,7 @@
 
 import Annex.Common
 import Types.Remote
+import Types.ProposedAccepted
 import Logs.UUIDBased
 import Annex.SpecialRemote.Config
 
@@ -40,24 +41,24 @@
 buildRemoteConfigLog = buildLogOld (byteString . encodeBS . showConfig)
 
 remoteConfigParser :: A.Parser RemoteConfig
-remoteConfigParser = keyValToConfig . words . decodeBS <$> A.takeByteString
+remoteConfigParser = keyValToConfig Accepted . words . decodeBS <$> A.takeByteString
 
 showConfig :: RemoteConfig -> String
 showConfig = unwords . configToKeyVal
 
 {- Given Strings like "key=value", generates a RemoteConfig. -}
-keyValToConfig :: [String] -> RemoteConfig
-keyValToConfig ws = M.fromList $ map (/=/) ws
+keyValToConfig :: (String -> ProposedAccepted String) -> [String] -> RemoteConfig
+keyValToConfig mk ws = M.fromList $ map (/=/) ws
   where
-	(/=/) s = (k, v)
+	(/=/) s = (mk k, mk v)
 	  where
 		k = takeWhile (/= '=') s
 		v = configUnEscape $ drop (1 + length k) s
 
-configToKeyVal :: M.Map String String -> [String]
+configToKeyVal :: RemoteConfig -> [String]
 configToKeyVal m = map toword $ sort $ M.toList m
   where
-	toword (k, v) = k ++ "=" ++ configEscape v
+	toword (k, v) = fromProposedAccepted k ++ "=" ++ configEscape (fromProposedAccepted v)
 
 configEscape :: String -> String
 configEscape = concatMap escape
@@ -90,9 +91,9 @@
 prop_parse_show_Config :: RemoteConfig -> Bool
 prop_parse_show_Config c
 	-- whitespace and '=' are not supported in config keys
-	| any (\k -> any isSpace k || elem '=' k) (M.keys c) = True
-	| any (any excluded) (M.keys c) = True
-	| any (any excluded) (M.elems c) = True
+	| any (\k -> any isSpace k || elem '=' k) (map fromProposedAccepted $ M.keys c) = True
+	| any (any excluded) (map fromProposedAccepted $ M.keys c) = True
+	| any (any excluded) (map fromProposedAccepted $ M.elems c) = True
 	| otherwise = A.parseOnly remoteConfigParser (encodeBS $ showConfig c) ~~ Right c
   where
 	normalize v = sort . M.toList <$> v
diff --git a/Messages.hs b/Messages.hs
--- a/Messages.hs
+++ b/Messages.hs
@@ -1,6 +1,6 @@
 {- git-annex output messages
  -
- - Copyright 2010-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -48,6 +48,7 @@
 	outputMessage,
 	withMessageState,
 	prompt,
+	mkPrompter,
 ) where
 
 import System.Log.Logger
@@ -55,6 +56,7 @@
 import System.Log.Handler (setFormatter)
 import System.Log.Handler.Simple
 import Control.Concurrent
+import Control.Monad.IO.Class
 import qualified Data.ByteString as S
 
 import Common
@@ -290,14 +292,21 @@
  - the user.
  -}
 prompt :: Annex a -> Annex a
-prompt a = debugLocks $ Annex.getState Annex.concurrency >>= \case
-	NonConcurrent -> a
+prompt a = do
+	p <- mkPrompter
+	p a
+
+{- Like prompt, but for a non-annex action that prompts. -}
+mkPrompter :: (MonadMask m, MonadIO m) => Annex (m a -> m a)
+mkPrompter = Annex.getState Annex.concurrency >>= \case
+	NonConcurrent -> return id
 	(Concurrent _) -> goconcurrent
 	ConcurrentPerCpu -> goconcurrent
   where
 	goconcurrent = withMessageState $ \s -> do
 		let l = promptLock s
-		bracketIO
-			(takeMVar l)
-			(putMVar l)
-			(const $ hideRegionsWhile s a)
+		return $ \a ->
+			debugLocks $ bracketIO
+				(takeMVar l)
+				(putMVar l)
+				(const $ hideRegionsWhile s a)
diff --git a/Messages/Concurrent.hs b/Messages/Concurrent.hs
--- a/Messages/Concurrent.hs
+++ b/Messages/Concurrent.hs
@@ -18,6 +18,7 @@
 import qualified System.Console.Concurrent as Console
 import qualified System.Console.Regions as Regions
 import Control.Concurrent.STM
+import Control.Monad.IO.Class
 import qualified Data.Text as T
 #ifndef mingw32_HOST_OS
 import GHC.IO.Encoding
@@ -120,13 +121,14 @@
 
 {- Hide any currently displayed console regions while running the action,
  - so that the action can use the console itself. -}
-hideRegionsWhile :: MessageState -> Annex a -> Annex a
-hideRegionsWhile s a 
-	| concurrentOutputEnabled s = bracketIO setup cleanup go
+hideRegionsWhile :: (MonadIO m, Monad m, MonadMask m) => MessageState -> m a -> m a
+hideRegionsWhile s a
+	| concurrentOutputEnabled s = bracket setup cleanup go
 	| otherwise = a
   where
-	setup = Regions.waitDisplayChange $ swapTMVar Regions.regionList []
-	cleanup = void . atomically . swapTMVar Regions.regionList
+	setup = liftIO $ 
+		Regions.waitDisplayChange $ swapTMVar Regions.regionList []
+	cleanup = liftIO . void . atomically . swapTMVar Regions.regionList
 	go _ = do
 		liftIO $ hFlush stdout
 		a
diff --git a/Remote/Adb.hs b/Remote/Adb.hs
--- a/Remote/Adb.hs
+++ b/Remote/Adb.hs
@@ -1,6 +1,6 @@
 {- Remote on Android device accessed using adb.
  -
- - Copyright 2018-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2018-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -19,6 +19,8 @@
 import Remote.Helper.ExportImport
 import Annex.UUID
 import Utility.Metered
+import Types.ProposedAccepted
+import Annex.SpecialRemote.Config
 
 import qualified Data.Map as M
 import qualified System.FilePath.Posix as Posix
@@ -31,16 +33,28 @@
 newtype AndroidPath = AndroidPath { fromAndroidPath :: FilePath }
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "adb"
 	, enumerate = const (findSpecialRemotes "adb")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser androiddirectoryField
+			(FieldDesc "location on the Android device where the files are stored")
+		, optionalStringParser androidserialField
+			(FieldDesc "sometimes needed to specify which Android device to use")
+		]
 	, setup = adbSetup
 	, exportSupported = exportIsSupported
 	, importSupported = importIsSupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+androiddirectoryField :: RemoteConfigField
+androiddirectoryField = Accepted "androiddirectory"
+
+androidserialField :: RemoteConfigField
+androidserialField = Accepted "androidserial"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	let this = Remote
 		{ uuid = u
@@ -109,10 +123,12 @@
 	u <- maybe (liftIO genUUID) return mu
 
 	-- verify configuration
-	adir <- maybe (giveup "Specify androiddirectory=") (pure . AndroidPath)
-		(M.lookup "androiddirectory" c)
+	adir <- maybe
+		(giveup "Specify androiddirectory=")
+		(pure . AndroidPath . fromProposedAccepted)
+		(M.lookup androiddirectoryField c)
 	serial <- getserial =<< liftIO enumerateAdbConnected
-	let c' = M.insert "androidserial" (fromAndroidSerial serial) c
+	let c' = M.insert androidserialField (Proposed (fromAndroidSerial serial)) c
 
 	(c'', _encsetup) <- encryptionSetup c' gc
 
@@ -130,7 +146,7 @@
 	return (c'', u)
   where
 	getserial [] = giveup "adb does not list any connected android devices. Plug in an Android device, or configure adb, and try again.."
-	getserial l = case M.lookup "androidserial" c of
+	getserial l = case fromProposedAccepted <$> M.lookup androidserialField c of
 		Nothing -> case l of
 			(s:[]) -> return s
 			_ -> giveup $ unlines $
diff --git a/Remote/BitTorrent.hs b/Remote/BitTorrent.hs
--- a/Remote/BitTorrent.hs
+++ b/Remote/BitTorrent.hs
@@ -41,6 +41,7 @@
 	{ typename = "bittorrent"
 	, enumerate = list
 	, generate = gen
+	, configParser = mkRemoteConfigParser []
 	, setup = error "not supported"
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
@@ -52,7 +53,7 @@
 	r <- liftIO $ Git.Construct.remoteNamed "bittorrent" (pure Git.Construct.fromUnknown)
 	return [r]
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r _ c gc rs = do
 	cst <- remoteCost gc expensiveRemoteCost
 	return $ Just Remote
diff --git a/Remote/Bup.hs b/Remote/Bup.hs
--- a/Remote/Bup.hs
+++ b/Remote/Bup.hs
@@ -1,6 +1,6 @@
 {- Using bup as a remote.
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -25,6 +25,7 @@
 import Config
 import Config.Cost
 import qualified Remote.Helper.Ssh as Ssh
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.Messages
 import Remote.Helper.ExportImport
@@ -33,20 +34,28 @@
 import Annex.UUID
 import Annex.Ssh
 import Utility.Metered
+import Types.ProposedAccepted
 
 type BupRepo = String
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "bup"
 	, enumerate = const (findSpecialRemotes "buprepo")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser buprepoField
+			(FieldDesc "(required) bup repository to use")
+		]
 	, setup = bupSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+buprepoField :: RemoteConfigField
+buprepoField = Accepted "buprepo"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	bupr <- liftIO $ bup2GitRemote buprepo
 	cst <- remoteCost gc $
@@ -108,8 +117,8 @@
 	u <- maybe (liftIO genUUID) return mu
 
 	-- verify configuration is sane
-	let buprepo = fromMaybe (giveup "Specify buprepo=") $
-		M.lookup "buprepo" c
+	let buprepo = maybe (giveup "Specify buprepo=") fromProposedAccepted $
+		M.lookup buprepoField c
 	(c', _encsetup) <- encryptionSetup c gc
 
 	-- bup init will create the repository.
diff --git a/Remote/Ddar.hs b/Remote/Ddar.hs
--- a/Remote/Ddar.hs
+++ b/Remote/Ddar.hs
@@ -18,11 +18,13 @@
 import qualified Git
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.ExportImport
 import Annex.Ssh
 import Annex.UUID
 import Utility.SshHost
+import Types.ProposedAccepted
 
 data DdarRepo = DdarRepo
 	{ ddarRepoConfig :: RemoteGitConfig
@@ -30,16 +32,23 @@
 	}
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "ddar"
 	, enumerate = const (findSpecialRemotes "ddarrepo")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser ddarrepoField
+			(FieldDesc "(required) location of ddar archive to use")
+		]
 	, setup = ddarSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+ddarrepoField :: RemoteConfigField
+ddarrepoField = Accepted "ddarrepo"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	cst <- remoteCost gc $
 		if ddarLocal ddarrepo
@@ -98,8 +107,8 @@
 	u <- maybe (liftIO genUUID) return mu
 
 	-- verify configuration is sane
-	let ddarrepo = fromMaybe (giveup "Specify ddarrepo=") $
-		M.lookup "ddarrepo" c
+	let ddarrepo = maybe (giveup "Specify ddarrepo=") fromProposedAccepted $
+		M.lookup ddarrepoField c
 	(c', _encsetup) <- encryptionSetup c gc
 
 	-- The ddarrepo is stored in git config, as well as this repo's
diff --git a/Remote/Directory.hs b/Remote/Directory.hs
--- a/Remote/Directory.hs
+++ b/Remote/Directory.hs
@@ -1,6 +1,6 @@
 {- A "remote" that is just a filesystem directory.
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -24,6 +24,7 @@
 import qualified Git
 import Config.Cost
 import Config
+import Annex.SpecialRemote.Config
 import Utility.FileMode
 import Remote.Helper.Special
 import Remote.Helper.ExportImport
@@ -34,18 +35,26 @@
 import Utility.Metered
 import Utility.Tmp
 import Utility.InodeCache
+import Types.ProposedAccepted
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "directory"
 	, enumerate = const (findSpecialRemotes "directory")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser directoryField
+			(FieldDesc "(required) where the special remote stores data")
+		]
 	, setup = directorySetup
 	, exportSupported = exportIsSupported
 	, importSupported = importIsSupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+directoryField :: RemoteConfigField
+directoryField = Accepted "directory"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	cst <- remoteCost gc cheapRemoteCost
 	let chunkconfig = getChunkConfig c
@@ -111,8 +120,8 @@
 directorySetup _ mu _ c gc = do
 	u <- maybe (liftIO genUUID) return mu
 	-- verify configuration is sane
-	let dir = fromMaybe (giveup "Specify directory=") $
-		M.lookup "directory" c
+	let dir = maybe (giveup "Specify directory=") fromProposedAccepted $
+		M.lookup directoryField c
 	absdir <- liftIO $ absPath dir
 	liftIO $ unlessM (doesDirectoryExist absdir) $
 		giveup $ "Directory does not exist: " ++ absdir
@@ -121,7 +130,7 @@
 	-- The directory is stored in git config, not in this remote's
 	-- persistant state, so it can vary between hosts.
 	gitConfigSpecialRemote u c' [("directory", absdir)]
-	return (M.delete "directory" c', u)
+	return (M.delete directoryField c', u)
 
 {- Locations to try to access a given Key in the directory.
  - We try more than one since we used to write to different hash
diff --git a/Remote/External.hs b/Remote/External.hs
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -1,6 +1,6 @@
 {- External special remote interface.
  -
- - Copyright 2013-2018 Joey Hess <id@joeyh.name>
+ - Copyright 2013-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -16,10 +16,12 @@
 import Types.Export
 import Types.CleanupActions
 import Types.UrlContents
+import Types.ProposedAccepted
 import qualified Git
 import Config
-import Git.Config (isTrueFalse, boolConfig)
+import Git.Config (boolConfig)
 import Git.Env
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.ExportImport
 import Remote.Helper.ReadOnly
@@ -41,18 +43,26 @@
 import Control.Concurrent.Async
 import System.Log.Logger (debugM)
 import qualified Data.Map as M
+import qualified Data.Set as S
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "external"
 	, enumerate = const (findSpecialRemotes "externaltype")
 	, generate = gen
+	, configParser = remoteConfigParser
 	, setup = externalSetup
 	, exportSupported = checkExportSupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+externaltypeField :: RemoteConfigField
+externaltypeField = Accepted "externaltype"
+
+readonlyField :: RemoteConfigField
+readonlyField = Accepted "readonly"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs
 	-- readonly mode only downloads urls; does not use external program
 	| remoteAnnexReadOnly gc = do
@@ -69,7 +79,7 @@
 			exportUnsupported
 			exportUnsupported
 	| otherwise = do
-		external <- newExternal externaltype u c gc (Just rs)
+		external <- newExternal externaltype (Just u) c (Just gc) (Just rs)
 		Annex.addCleanup (RemoteCleanup u) $ stopExternal external
 		cst <- getCost external r gc
 		avail <- getAvailability external r gc
@@ -152,32 +162,43 @@
 externalSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
 externalSetup _ mu _ c gc = do
 	u <- maybe (liftIO genUUID) return mu
+	pc <- either giveup return $ parseRemoteConfig c lenientRemoteConfigParser
 	let externaltype = fromMaybe (giveup "Specify externaltype=") $
-		M.lookup "externaltype" c
+		getRemoteConfigValue externaltypeField pc
 	(c', _encsetup) <- encryptionSetup c gc
 
-	c'' <- case M.lookup "readonly" c of
-		Just v | isTrueFalse v == Just True -> do
+	c'' <- case getRemoteConfigValue readonlyField pc of
+		Just True -> do
 			setConfig (remoteConfig (fromJust (lookupName c)) "readonly") (boolConfig True)
 			return c'
 		_ -> do
-			external <- newExternal externaltype u c' gc Nothing
+			pc' <- either giveup return $ parseRemoteConfig c' lenientRemoteConfigParser
+			external <- newExternal externaltype (Just u) pc' (Just gc) Nothing
+			-- Now that we have an external, ask it to LISTCONFIGS, 
+			-- and re-parse the RemoteConfig strictly, so we can
+			-- error out if the user provided an unexpected config.
+			_ <- either giveup return . parseRemoteConfig c' 
+				=<< strictRemoteConfigParser external
 			handleRequest external INITREMOTE Nothing $ \resp -> case resp of
 				INITREMOTE_SUCCESS -> result ()
 				INITREMOTE_FAILURE errmsg -> Just $ giveup errmsg
 				_ -> Nothing
-			withExternalState external $
-				liftIO . atomically . readTVar . externalConfig
+			-- Any config changes the external made before
+			-- responding to INITREMOTE need to be applied to
+			-- the RemoteConfig.
+			changes <- withExternalState external $
+				liftIO . atomically . readTVar . externalConfigChanges
+			return (changes c')
 
 	gitConfigSpecialRemote u c'' [("externaltype", externaltype)]
 	return (c'', u)
 
-checkExportSupported :: RemoteConfig -> RemoteGitConfig -> Annex Bool
+checkExportSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
 checkExportSupported c gc = do
 	let externaltype = fromMaybe (giveup "Specify externaltype=") $
-		remoteAnnexExternalType gc <|> M.lookup "externaltype" c
+		remoteAnnexExternalType gc <|> getRemoteConfigValue externaltypeField c
 	checkExportSupported' 
-		=<< newExternal externaltype NoUUID c gc Nothing
+		=<< newExternal externaltype Nothing c (Just gc) Nothing
 
 checkExportSupported' :: External -> Annex Bool
 checkExportSupported' external = go `catchNonAsync` (const (return False))
@@ -387,36 +408,48 @@
 	handleRemoteRequest (DIRHASH_LOWER k) = 
 		send $ VALUE $ fromRawFilePath $ hashDirLower def k
 	handleRemoteRequest (SETCONFIG setting value) =
-		liftIO $ atomically $ modifyTVar' (externalConfig st) $
-			M.insert setting value
+		liftIO $ atomically $ do
+			modifyTVar' (externalConfig st) $
+				M.insert (Accepted setting) $
+					RemoteConfigValue (PassedThrough value)
+			modifyTVar' (externalConfigChanges st) $ \f ->
+				f . M.insert (Accepted setting) (Accepted value)
 	handleRemoteRequest (GETCONFIG setting) = do
-		value <- fromMaybe "" . M.lookup setting
+		value <- fromMaybe ""
+			. M.lookup (Accepted setting)
+			. getRemoteConfigPassedThrough
 			<$> liftIO (atomically $ readTVar $ externalConfig st)
 		send $ VALUE value
-	handleRemoteRequest (SETCREDS setting login password) = do
-		let v = externalConfig st
-		c <- liftIO $ atomically $ readTVar v
-		let gc = externalGitConfig external
-		c' <- setRemoteCredPair encryptionAlreadySetup c gc
-			(credstorage setting)
-			(Just (login, password))
-		void $ liftIO $ atomically $ swapTVar v c'
-	handleRemoteRequest (GETCREDS setting) = do
-		c <- liftIO $ atomically $ readTVar $ externalConfig st
-		let gc = externalGitConfig external
-		creds <- fromMaybe ("", "") <$> 
-			getRemoteCredPair c gc (credstorage setting)
-		send $ CREDS (fst creds) (snd creds)
-	handleRemoteRequest GETUUID = send $
-		VALUE $ fromUUID $ externalUUID external
+	handleRemoteRequest (SETCREDS setting login password) = case (externalUUID external, externalGitConfig external) of
+		(Just u, Just gc) -> do
+			let v = externalConfig st
+			c <- liftIO $ atomically $ readTVar v
+			c' <- setRemoteCredPair' RemoteConfigValue id encryptionAlreadySetup c gc
+				(credstorage setting u)
+				(Just (login, password))
+			void $ liftIO $ atomically $ swapTVar v c'
+		_ -> senderror "cannot send SETCREDS here"
+	handleRemoteRequest (GETCREDS setting) = case (externalUUID external, externalGitConfig external) of
+		(Just u, Just gc) -> do
+			c <- liftIO $ atomically $ readTVar $ externalConfig st
+			creds <- fromMaybe ("", "") <$> 
+				getRemoteCredPair c gc (credstorage setting u)
+			send $ CREDS (fst creds) (snd creds)
+		_ -> senderror "cannot send GETCREDS here"
+	handleRemoteRequest GETUUID = case externalUUID external of
+		Just u -> send $ VALUE $ fromUUID u
+		Nothing -> senderror "cannot send GETUUID here"
 	handleRemoteRequest GETGITDIR = 
 		send . VALUE . fromRawFilePath =<< fromRepo Git.localGitDir
-	handleRemoteRequest (SETWANTED expr) =
-		preferredContentSet (externalUUID external) expr
-	handleRemoteRequest GETWANTED = do
-		expr <- fromMaybe "" . M.lookup (externalUUID external)
-			<$> preferredContentMapRaw
-		send $ VALUE expr
+	handleRemoteRequest (SETWANTED expr) = case externalUUID external of
+		Just u -> preferredContentSet u expr
+		Nothing -> senderror "cannot send SETWANTED here"
+	handleRemoteRequest GETWANTED = case externalUUID external of
+		Just u -> do
+			expr <- fromMaybe "" . M.lookup u
+				<$> preferredContentMapRaw
+			send $ VALUE expr
+		Nothing -> senderror "cannot send GETWANTED here"
 	handleRemoteRequest (SETSTATE key state) =
 		case externalRemoteStateHandle external of
 			Just h -> setRemoteState h key state
@@ -448,13 +481,13 @@
 	send = sendMessage st external
 	senderror = sendMessage st external . ERROR 
 
-	credstorage setting = CredPairStorage
+	credstorage setting u = CredPairStorage
 		{ credPairFile = base
 		, credPairEnvironment = (base ++ "login", base ++ "password")
-		, credPairRemoteField = setting
+		, credPairRemoteField = Accepted setting
 		}
 	  where
-		base = replace "/" "_" $ fromUUID (externalUUID external) ++ "-" ++ setting
+		base = replace "/" "_" $ fromUUID u ++ "-" ++ setting
 			
 	withurl mk uri = handleRemoteRequest $ mk $
 		setDownloader (show uri) OtherDownloader
@@ -579,6 +612,7 @@
 			createProcess p `catchIO` runerr cmdpath
 		stderrelay <- async $ errrelayer herr
 		cv <- newTVarIO $ externalDefaultConfig external
+		ccv <- newTVarIO id
 		pv <- newTVarIO Unprepared
 		pid <- atomically $ do
 			n <- succ <$> readTVar (externalLastPid external)
@@ -593,6 +627,7 @@
 				void $ waitForProcess ph
 			, externalPrepared = pv
 			, externalConfig = cv
+			, externalConfigChanges = ccv
 			}
 	
 	basecmd = externalRemoteProgram $ externalType external
@@ -712,7 +747,7 @@
 retrieveUrl :: Retriever
 retrieveUrl = fileRetriever $ \f k p -> do
 	us <- getWebUrls k
-	unlessM (downloadUrl k p us f) $
+	unlessM (withUrlOptions $ downloadUrl k p us f) $
 		giveup "failed to download content"
 
 checkKeyUrl :: Git.Repo -> CheckPresent
@@ -745,3 +780,63 @@
 		INFOVALUE v -> Just $ return $
 			GetNextMessage $ collect ((f, v) : l)
 		_ -> Nothing
+
+{- All unknown configs are passed through in case the external program
+ - uses them. -}
+lenientRemoteConfigParser :: RemoteConfigParser
+lenientRemoteConfigParser =
+	addRemoteConfigParser specialRemoteConfigParsers baseRemoteConfigParser
+
+baseRemoteConfigParser :: RemoteConfigParser
+baseRemoteConfigParser = RemoteConfigParser
+	{ remoteConfigFieldParsers =
+		[ optionalStringParser externaltypeField
+			(FieldDesc "type of external special remote to use")
+		, trueFalseParser readonlyField False
+			(FieldDesc "enable readonly mode")
+		]
+	, remoteConfigRestPassthrough = Just
+		( const True
+		, [("*", FieldDesc "all other parameters are passed to external special remote program")]
+		)
+	}
+
+{- When the remote supports LISTCONFIGS, only accept the ones it listed.
+ - When it does not, accept all configs. -}
+strictRemoteConfigParser :: External -> Annex RemoteConfigParser
+strictRemoteConfigParser external = listConfigs external >>= \case
+	Nothing -> return lenientRemoteConfigParser
+	Just l -> do
+		let s = S.fromList (map fst l)
+		let listed f = S.member (fromProposedAccepted f) s
+		return $ lenientRemoteConfigParser
+			{ remoteConfigRestPassthrough = Just (listed, l) }
+
+listConfigs :: External -> Annex (Maybe [(Setting, FieldDesc)])
+listConfigs external = handleRequest external LISTCONFIGS Nothing (collect [])
+  where
+	collect l req = case req of
+		CONFIG s d -> Just $ return $
+			GetNextMessage $ collect ((s, FieldDesc d) : l)
+		CONFIGEND -> result (Just (reverse l))
+		UNSUPPORTED_REQUEST -> result Nothing
+		_ -> Nothing
+
+remoteConfigParser :: RemoteConfig -> Annex RemoteConfigParser
+remoteConfigParser c
+	-- No need to start the external when there is no config to parse,
+	-- or when everything in the config was already accepted; in those
+	-- cases the lenient parser will do the same thing as the strict
+	-- parser.
+	| M.null (M.filter isproposed c) = return lenientRemoteConfigParser
+	| otherwise = case parseRemoteConfig c baseRemoteConfigParser of
+		Left _ -> return lenientRemoteConfigParser
+		Right pc -> case (getRemoteConfigValue externaltypeField pc, getRemoteConfigValue readonlyField pc) of
+			(Nothing, _) -> return lenientRemoteConfigParser
+			(_, Just True) -> return lenientRemoteConfigParser
+			(Just externaltype, _) -> do
+				external <- newExternal externaltype Nothing pc Nothing Nothing
+				strictRemoteConfigParser external
+  where
+	isproposed (Accepted _) = False
+	isproposed (Proposed _) = True
diff --git a/Remote/External/Types.hs b/Remote/External/Types.hs
--- a/Remote/External/Types.hs
+++ b/Remote/External/Types.hs
@@ -1,6 +1,6 @@
 {- External special remote data types.
  -
- - Copyright 2013-2018 Joey Hess <id@joeyh.name>
+ - Copyright 2013-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -28,6 +28,7 @@
 	AsyncMessage(..),
 	ErrorMsg,
 	Setting,
+	Description,
 	ProtocolVersion,
 	supportedProtocolVersions,
 ) where
@@ -37,7 +38,8 @@
 import Utility.Metered (BytesProcessed(..))
 import Types.Transfer (Direction(..))
 import Config.Cost (Cost)
-import Types.Remote (RemoteConfig, RemoteStateHandle)
+import Types.RemoteState
+import Types.RemoteConfig
 import Types.Export
 import Types.Availability (Availability(..))
 import Types.Key
@@ -50,17 +52,17 @@
 
 data External = External
 	{ externalType :: ExternalType
-	, externalUUID :: UUID
+	, externalUUID :: Maybe UUID
 	, externalState :: TVar [ExternalState]
 	-- ^ Contains states for external special remote processes
 	-- that are not currently in use.
 	, externalLastPid :: TVar PID
-	, externalDefaultConfig :: RemoteConfig
-	, externalGitConfig :: RemoteGitConfig
+	, externalDefaultConfig :: ParsedRemoteConfig
+	, externalGitConfig :: Maybe RemoteGitConfig
 	, externalRemoteStateHandle :: Maybe RemoteStateHandle
 	}
 
-newExternal :: ExternalType -> UUID -> RemoteConfig -> RemoteGitConfig -> Maybe RemoteStateHandle -> Annex External
+newExternal :: ExternalType -> Maybe UUID -> ParsedRemoteConfig -> Maybe RemoteGitConfig -> Maybe RemoteStateHandle -> Annex External
 newExternal externaltype u c gc rs = liftIO $ External
 	<$> pure externaltype
 	<*> pure u
@@ -78,7 +80,8 @@
 	, externalShutdown :: IO ()
 	, externalPid :: PID
 	, externalPrepared :: TVar PrepareStatus
-	, externalConfig :: TVar RemoteConfig
+	, externalConfig :: TVar ParsedRemoteConfig
+	, externalConfigChanges :: TVar (RemoteConfig -> RemoteConfig)
 	}
 
 type PID = Int
@@ -129,6 +132,7 @@
 	| CHECKPRESENT SafeKey
 	| REMOVE SafeKey
 	| WHEREIS SafeKey
+	| LISTCONFIGS
 	| GETINFO
 	| EXPORTSUPPORTED
 	| EXPORT ExportLocation
@@ -145,6 +149,7 @@
 needsPREPARE (EXTENSIONS _) = False
 needsPREPARE INITREMOTE = False
 needsPREPARE EXPORTSUPPORTED = False
+needsPREPARE LISTCONFIGS = False
 needsPREPARE _ = True
 
 instance Proto.Sendable Request where
@@ -165,6 +170,7 @@
 		[ "CHECKPRESENT", Proto.serialize key ]
 	formatMessage (REMOVE key) = [ "REMOVE", Proto.serialize key ]
 	formatMessage (WHEREIS key) = [ "WHEREIS", Proto.serialize key ]
+	formatMessage LISTCONFIGS = [ "LISTCONFIGS" ]
 	formatMessage GETINFO = [ "GETINFO" ]
 	formatMessage EXPORTSUPPORTED = ["EXPORTSUPPORTED"]
 	formatMessage (EXPORT loc) = [ "EXPORT", Proto.serialize loc ]
@@ -209,6 +215,8 @@
 	| CHECKURL_FAILURE ErrorMsg
 	| WHEREIS_SUCCESS String
 	| WHEREIS_FAILURE
+	| CONFIG Setting Description
+	| CONFIGEND
 	| INFOFIELD String
 	| INFOVALUE String
 	| INFOEND
@@ -243,6 +251,8 @@
 	parseCommand "CHECKURL-FAILURE" = Proto.parse1 CHECKURL_FAILURE
 	parseCommand "WHEREIS-SUCCESS" = Just . WHEREIS_SUCCESS
 	parseCommand "WHEREIS-FAILURE" = Proto.parse0 WHEREIS_FAILURE
+	parseCommand "CONFIG" = Proto.parse2 CONFIG
+	parseCommand "CONFIGEND" = Proto.parse0 CONFIGEND
 	parseCommand "INFOFIELD" = Proto.parse1 INFOFIELD
 	parseCommand "INFOVALUE" = Proto.parse1 INFOVALUE
 	parseCommand "INFOEND" = Proto.parse0 INFOEND
@@ -330,6 +340,7 @@
 -- All are serializable.
 type ErrorMsg = String
 type Setting = String
+type Description = String
 type ProtocolVersion = Int
 type Size = Maybe Integer
 
diff --git a/Remote/GCrypt.hs b/Remote/GCrypt.hs
--- a/Remote/GCrypt.hs
+++ b/Remote/GCrypt.hs
@@ -39,6 +39,7 @@
 import qualified Annex.Branch
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Remote.Helper.Git
 import Remote.Helper.Encryptable
 import Remote.Helper.Special
@@ -55,21 +56,31 @@
 import Logs.Remote
 import Utility.Gpg
 import Utility.SshHost
+import Utility.Tuple
 import Messages.Progress
+import Types.ProposedAccepted
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "gcrypt"
 	-- Remote.Git takes care of enumerating gcrypt remotes too,
 	-- and will call our gen on them.
 	, enumerate = const (return [])
 	, generate = gen
+	, configParser = mkRemoteConfigParser $
+		Remote.Rsync.rsyncRemoteConfigs ++
+		[ optionalStringParser gitRepoField
+			(FieldDesc "(required) path or url to gcrypt repository")
+		]
 	, setup = gCryptSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-chainGen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gitRepoField :: RemoteConfigField
+gitRepoField = Accepted "gitrepo"
+
+chainGen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 chainGen gcryptr u c gc rs = do
 	g <- gitRepo
 	-- get underlying git repo with real path, not gcrypt path
@@ -77,7 +88,7 @@
 	let r' = r { Git.remoteName = Git.remoteName gcryptr }
 	gen r' u c gc rs
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen baser u c gc rs = do
 	-- doublecheck that cache matches underlying repo's gcrypt-id
 	-- (which might not be set), only for local repos
@@ -98,15 +109,18 @@
 		v <- M.lookup u' <$> readRemoteLog
 		case (Git.remoteName baser, v) of
 			(Just remotename, Just c') -> do
-				setGcryptEncryption c' remotename
+				pc <- either giveup return
+					. parseRemoteConfig c'
+					=<< configParser remote c'
+				setGcryptEncryption pc remotename
 				storeUUIDIn (remoteConfig baser "uuid") u'
 				setConfig (Git.GCrypt.remoteConfigKey "gcrypt-id" remotename) gcryptid
-				gen' r u' c' gc rs
+				gen' r u' pc gc rs
 			_ -> do
 				warning $ "not using unknown gcrypt repository pointed to by remote " ++ Git.repoDescribe r
 				return Nothing
 
-gen' :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gen' :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen' r u c gc rs = do
 	cst <- remoteCost gc $
 		if repoCheap r then nearlyCheapRemoteCost else expensiveRemoteCost
@@ -187,7 +201,7 @@
 unsupportedUrl = giveup "using non-ssh remote repo url with gcrypt is not supported"
 
 gCryptSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
-gCryptSetup _ mu _ c gc = go $ M.lookup "gitrepo" c
+gCryptSetup _ mu _ c gc = go $ fromProposedAccepted <$> M.lookup gitRepoField c
   where
 	remotename = fromJust (lookupName c)
 	go Nothing = giveup "Specify gitrepo="
@@ -206,7 +220,9 @@
 				| Git.repoLocation r == url -> noop
 				| otherwise -> error "Another remote with the same name already exists."		
 
-		setGcryptEncryption c' remotename
+		pc <- either giveup return . parseRemoteConfig c'
+			=<< configParser remote c'
+		setGcryptEncryption pc remotename
 
 		{- Run a git fetch and a push to the git repo in order to get
 		 - its gcrypt-id set up, so that later git annex commands
@@ -322,7 +338,7 @@
  - Also, sets gcrypt-publish-participants to avoid unncessary gpg
  - passphrase prompts.
  -}
-setGcryptEncryption :: RemoteConfig -> String -> Annex ()
+setGcryptEncryption :: ParsedRemoteConfig -> String -> Annex ()
 setGcryptEncryption c remotename = do
 	let participants = remoteconfig Git.GCrypt.remoteParticipantConfigKey
 	case extractCipher c of
@@ -456,7 +472,7 @@
 getGCryptId fast r gc
 	| Git.repoIsLocal r || Git.repoIsLocalUnknown r = extract <$>
 		liftIO (catchMaybeIO $ Git.Config.read r)
-	| not fast = extract . liftM fst <$> getM (eitherToMaybe <$>)
+	| not fast = extract . liftM fst3 <$> getM (eitherToMaybe <$>)
 		[ Ssh.onRemote NoConsumeStdin r (\f p -> liftIO (Git.Config.fromPipe r f p), return (Left $ error "configlist failed")) "configlist" [] []
 		, getConfigViaRsync r gc
 		]
@@ -465,7 +481,7 @@
 	extract Nothing = (Nothing, r)
 	extract (Just r') = (fromConfigValue <$> Git.Config.getMaybe coreGCryptId r', r')
 
-getConfigViaRsync :: Git.Repo -> RemoteGitConfig -> Annex (Either SomeException (Git.Repo, S.ByteString))
+getConfigViaRsync :: Git.Repo -> RemoteGitConfig -> Annex (Either SomeException (Git.Repo, S.ByteString, S.ByteString))
 getConfigViaRsync r gc = do
 	(rsynctransport, rsyncurl, _) <- rsyncTransport r gc
 	opts <- rsynctransport
diff --git a/Remote/Git.hs b/Remote/Git.hs
--- a/Remote/Git.hs
+++ b/Remote/Git.hs
@@ -37,6 +37,7 @@
 import Utility.Tmp
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Config.DynamicConfig
 import Annex.Init
 import Types.CleanupActions
@@ -59,11 +60,12 @@
 import Annex.Path
 import Creds
 import Types.NumCopies
+import Types.ProposedAccepted
 import Annex.Action
 import Messages.Progress
-import qualified Utility.RawFilePath as R
 
 #ifndef mingw32_HOST_OS
+import qualified Utility.RawFilePath as R
 import Utility.FileMode
 #endif
 
@@ -78,11 +80,18 @@
 	{ typename = "git"
 	, enumerate = list
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser locationField
+			(FieldDesc "url of git remote to remember with special remote")
+		]
 	, setup = gitSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
+locationField :: RemoteConfigField
+locationField = Accepted "location"
+
 list :: Bool -> Annex [Git.Repo]
 list autoinit = do
 	c <- fromRepo Git.config
@@ -111,7 +120,8 @@
 gitSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
 gitSetup Init mu _ c _ = do
 	let location = fromMaybe (giveup "Specify location=url") $
-		Url.parseURIRelaxed =<< M.lookup "location" c
+		Url.parseURIRelaxed . fromProposedAccepted
+			=<< M.lookup locationField c
 	rs <- Annex.getGitRemotes
 	u <- case filter (\r -> Git.location r == Git.Url location) rs of
 		[r] -> getRepoUUID r
@@ -125,7 +135,7 @@
 		[ Param "remote"
 		, Param "add"
 		, Param $ fromMaybe (giveup "no name") (SpecialRemote.lookupName c)
-		, Param $ fromMaybe (giveup "no location") (M.lookup "location" c)
+		, Param $ maybe (giveup "no location") fromProposedAccepted (M.lookup locationField c)
 		]
 	return (c, u)
 gitSetup (Enable _) Nothing _ _ _ = error "unable to enable git remote with no specified uuid"
@@ -151,7 +161,7 @@
 			Just r' -> return r'
 		_ -> return r
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs
 	-- Remote.GitLFS may be used with a repo that is also encrypted
 	-- with gcrypt so is checked first.
@@ -202,7 +212,7 @@
 			, remoteStateHandle = rs
 			}
 
-unavailable :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+unavailable :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 unavailable r = gen r'
   where
 	r' = case Git.location r of
@@ -238,7 +248,7 @@
 	| haveconfig r = return r -- already read
 	| Git.repoIsSsh r = storeUpdatedRemote $ do
 		v <- Ssh.onRemote NoConsumeStdin r
-			(pipedconfig, return (Left $ giveup "configlist failed"))
+			(pipedconfig autoinit (Git.repoDescribe r), return (Left $ giveup "configlist failed"))
 			"configlist" [] configlistfields
 		case v of
 			Right r'
@@ -253,23 +263,25 @@
   where
 	haveconfig = not . M.null . Git.config
 
-	pipedconfig cmd params = do
+	pipedconfig mustincludeuuuid configloc cmd params = do
 		v <- liftIO $ Git.Config.fromPipe r cmd params
 		case v of
-			Right (r', val) -> do
-				unless (isUUIDConfigured r' || S.null val) $ do
+			Right (r', val, _err) -> do
+				unless (isUUIDConfigured r' || S.null val || not mustincludeuuuid) $ do
 					warning $ "Failed to get annex.uuid configuration of repository " ++ Git.repoDescribe r
 					warning $ "Instead, got: " ++ show val
 					warning $ "This is unexpected; please check the network transport!"
 				return $ Right r'
-			Left l -> return $ Left l
+			Left l -> do
+				warning $ "Unable to parse git config from " ++ configloc
+				return $ Left l
 
-	geturlconfig = Url.withUrlOptions $ \uo -> do
+	geturlconfig = Url.withUrlOptionsPromptingCreds $ \uo -> do
 		v <- withTmpFile "git-annex.tmp" $ \tmpfile h -> do
 			liftIO $ hClose h
 			let url = Git.repoLocation r ++ "/config"
 			ifM (liftIO $ Url.downloadQuiet nullMeterUpdate url tmpfile uo)
-				( Just <$> pipedconfig "git" [Param "config", Param "--null", Param "--list", Param "--file", File tmpfile]
+				( Just <$> pipedconfig False url "git" [Param "config", Param "--null", Param "--list", Param "--file", File tmpfile]
 				, return Nothing
 				)
 		case v of
@@ -370,7 +382,7 @@
 	checkhttp = do
 		showChecking repo
 		gc <- Annex.getGitConfig
-		ifM (Url.withUrlOptions $ \uo -> anyM (\u -> Url.checkBoth u (fromKey keySize key) uo) (keyUrls gc repo rmt key))
+		ifM (Url.withUrlOptionsPromptingCreds $ \uo -> anyM (\u -> Url.checkBoth u (fromKey keySize key) uo) (keyUrls gc repo rmt key))
 			( return True
 			, giveup "not found"
 			)
@@ -502,7 +514,8 @@
 copyFromRemote'' repo forcersync r st@(State connpool _ _ _) key file dest meterupdate
 	| Git.repoIsHttp repo = unVerified $ do
 		gc <- Annex.getGitConfig
-		Annex.Content.downloadUrl key meterupdate (keyUrls gc repo r key) dest
+		Url.withUrlOptionsPromptingCreds $
+			Annex.Content.downloadUrl key meterupdate (keyUrls gc repo r key) dest
 	| not $ Git.repoIsUrl repo = guardUsable repo (unVerified (return False)) $ do
 		params <- Ssh.rsyncParams r Download
 		u <- getUUID
diff --git a/Remote/GitLFS.hs b/Remote/GitLFS.hs
--- a/Remote/GitLFS.hs
+++ b/Remote/GitLFS.hs
@@ -1,6 +1,6 @@
 {- Using git-lfs as a remote.
  -
- - Copyright 2019 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -14,6 +14,7 @@
 import Annex.Url
 import Types.Key
 import Types.Creds
+import Types.ProposedAccepted
 import qualified Annex
 import qualified Annex.SpecialRemote.Config
 import qualified Git
@@ -24,6 +25,7 @@
 import qualified Git.Credential as Git
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.ExportImport
 import Remote.Helper.Git
@@ -35,6 +37,7 @@
 import Backend.Hash
 import Utility.Hash
 import Utility.SshHost
+import Utility.Url
 import Logs.Remote
 import Logs.RemoteState
 import qualified Utility.GitLFS as LFS
@@ -52,18 +55,25 @@
 import qualified Control.Concurrent.MSemN as MSemN
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "git-lfs"
 	-- Remote.Git takes care of enumerating git-lfs remotes too,
 	-- and will call our gen on them.
 	, enumerate = const (return [])
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser urlField
+			(FieldDesc "url of git-lfs repository")
+		]
 	, setup = mySetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+urlField :: RemoteConfigField
+urlField = Accepted "url"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	-- If the repo uses gcrypt, get the underlaying repo without the
 	-- gcrypt url, to do LFS endpoint discovery on.
@@ -127,9 +137,10 @@
 	u <- maybe (liftIO genUUID) return mu
 
 	(c', _encsetup) <- encryptionSetup c gc
-	case (isEncrypted c', Git.GCrypt.urlPrefix `isPrefixOf` url) of
+	pc <- either giveup return . parseRemoteConfig c' =<< configParser remote c'
+	case (isEncrypted pc, Git.GCrypt.urlPrefix `isPrefixOf` url) of
 		(False, False) -> noop
-		(True, True) -> Remote.GCrypt.setGcryptEncryption c' remotename
+		(True, True) -> Remote.GCrypt.setGcryptEncryption pc remotename
 		(True, False) -> unlessM (Annex.getState Annex.force) $
 			giveup $ unwords $
 				[ "Encryption is enabled for this remote,"
@@ -158,7 +169,8 @@
 	setConfig (Git.ConfigKey ("remote." <> encodeBS' (getRemoteName c) <> ".url")) url
 	return (c', u)
   where
-	url = fromMaybe (giveup "Specify url=") (M.lookup "url" c)
+	url = maybe (giveup "Specify url=") fromProposedAccepted 
+		(M.lookup urlField c)
 	remotename = fromJust (lookupName c)
 
 {- Check if a remote's url is one known to belong to a git-lfs repository.
@@ -175,8 +187,10 @@
 	| otherwise = return Nothing
   where
 	match g c = fromMaybe False $ do
-		t <- M.lookup Annex.SpecialRemote.Config.typeField c
-		u <- M.lookup "url" c
+		t <- fromProposedAccepted
+			<$> M.lookup Annex.SpecialRemote.Config.typeField c
+		u <- fromProposedAccepted
+			<$> M.lookup urlField c
 		let u' = Git.Remote.parseRemoteLocation u g
 		return $ Git.Remote.RemoteUrl (Git.repoLocation r) == u' 
 			&& t == typename remote
@@ -270,7 +284,7 @@
 				if needauth (responseStatus resp)
 					then do
 						cred <- prompt $ inRepo $ Git.getUrlCredential (show lfsrepouri)
-						let endpoint' = addbasicauth cred endpoint
+						let endpoint' = addbasicauth (Git.credentialBasicAuth cred) endpoint
 						let testreq' = LFS.startTransferRequest endpoint' transfernothing
 						flip catchNonAsync (const (returnendpoint endpoint')) $ do
 						resp' <- makeSmallAPIRequest testreq'
@@ -290,12 +304,10 @@
 
 		needauth status = status == unauthorized401
 
-		addbasicauth cred endpoint =
-			case (Git.credentialUsername cred, Git.credentialPassword cred) of
-				(Just u, Just p) ->
-					LFS.modifyEndpointRequest endpoint $
-						applyBasicAuth (encodeBS u) (encodeBS p)
-				_ -> endpoint
+		addbasicauth (Just ba) endpoint =
+			LFS.modifyEndpointRequest endpoint $
+				applyBasicAuth' ba
+		addbasicauth Nothing endpoint = endpoint
 
 -- The endpoint is cached for later use.
 getLFSEndpoint :: LFS.TransferRequestOperation -> TVar LFSHandle -> Annex (Maybe LFS.Endpoint)
diff --git a/Remote/Glacier.hs b/Remote/Glacier.hs
--- a/Remote/Glacier.hs
+++ b/Remote/Glacier.hs
@@ -1,6 +1,6 @@
 {- Amazon Glacier remotes.
  -
- - Copyright 2012 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -16,6 +16,7 @@
 import qualified Git
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.Messages
 import Remote.Helper.ExportImport
@@ -25,21 +26,40 @@
 import qualified Annex
 import Annex.UUID
 import Utility.Env
+import Types.ProposedAccepted
 
 type Vault = String
 type Archive = FilePath
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "glacier"
 	, enumerate = const (findSpecialRemotes "glacier")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser datacenterField
+			(FieldDesc "S3 datacenter to use")
+		, optionalStringParser vaultField
+			(FieldDesc "name to use for vault")
+		, optionalStringParser fileprefixField
+			(FieldDesc "prefix to add to filenames in the vault")
+		, optionalStringParser AWS.s3credsField HiddenField
+		]
 	, setup = glacierSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+datacenterField :: RemoteConfigField
+datacenterField = Accepted "datacenter"
+	
+vaultField :: RemoteConfigField
+vaultField = Accepted "vault"
+
+fileprefixField :: RemoteConfigField
+fileprefixField = Accepted "fileprefix"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = new <$> remoteCost gc veryExpensiveRemoteCost
   where
 	new cst = Just $ specialRemote' specialcfg c
@@ -99,8 +119,10 @@
 	(c', encsetup) <- encryptionSetup c gc
 	c'' <- setRemoteCredPair encsetup c' gc (AWS.creds u) mcreds
 	let fullconfig = c'' `M.union` defaults
+	pc <- either giveup return . parseRemoteConfig fullconfig
+		=<< configParser remote fullconfig
 	case ss of
-		Init -> genVault fullconfig gc u
+		Init -> genVault pc gc u
 		_ -> return ()
 	gitConfigSpecialRemote u fullconfig [("glacier", "true")]
 	return (fullconfig, u)
@@ -108,8 +130,8 @@
 	remotename = fromJust (lookupName c)
 	defvault = remotename ++ "-" ++ fromUUID u
 	defaults = M.fromList
-		[ ("datacenter", T.unpack $ AWS.defaultRegion AWS.Glacier)
-		, ("vault", defvault)
+		[ (datacenterField, Proposed $ T.unpack $ AWS.defaultRegion AWS.Glacier)
+		, (vaultField, Proposed defvault)
 		]
 
 prepareStore :: Remote -> Preparer Storer
@@ -224,21 +246,21 @@
 glacierAction :: Remote -> [CommandParam] -> Annex Bool
 glacierAction r = runGlacier (config r) (gitconfig r) (uuid r)
 
-runGlacier :: RemoteConfig -> RemoteGitConfig -> UUID -> [CommandParam] -> Annex Bool
+runGlacier :: ParsedRemoteConfig -> RemoteGitConfig -> UUID -> [CommandParam] -> Annex Bool
 runGlacier c gc u params = go =<< glacierEnv c gc u
   where
 	go Nothing = return False
 	go (Just e) = liftIO $
 		boolSystemEnv "glacier" (glacierParams c params) (Just e)
 
-glacierParams :: RemoteConfig -> [CommandParam] -> [CommandParam]
+glacierParams :: ParsedRemoteConfig -> [CommandParam] -> [CommandParam]
 glacierParams c params = datacenter:params
   where
 	datacenter = Param $ "--region=" ++
 		fromMaybe (giveup "Missing datacenter configuration")
-			(M.lookup "datacenter" c)
+			(getRemoteConfigValue datacenterField c)
 
-glacierEnv :: RemoteConfig -> RemoteGitConfig -> UUID -> Annex (Maybe [(String, String)])
+glacierEnv :: ParsedRemoteConfig -> RemoteGitConfig -> UUID -> Annex (Maybe [(String, String)])
 glacierEnv c gc u = do
 	liftIO checkSaneGlacierCommand
 	go =<< getRemoteCredPairFor "glacier" c gc creds
@@ -251,16 +273,17 @@
 	creds = AWS.creds u
 	(uk, pk) = credPairEnvironment creds
 
-getVault :: RemoteConfig -> Vault
+getVault :: ParsedRemoteConfig -> Vault
 getVault = fromMaybe (giveup "Missing vault configuration") 
-	. M.lookup "vault"
+	. getRemoteConfigValue vaultField
 
 archive :: Remote -> Key -> Archive
 archive r k = fileprefix ++ serializeKey k
   where
-	fileprefix = M.findWithDefault "" "fileprefix" $ config r
+	fileprefix = fromMaybe "" $
+		getRemoteConfigValue fileprefixField $ config r
 
-genVault :: RemoteConfig -> RemoteGitConfig -> UUID -> Annex ()
+genVault :: ParsedRemoteConfig -> RemoteGitConfig -> UUID -> Annex ()
 genVault c gc u = unlessM (runGlacier c gc u params) $
 	giveup "Failed creating glacier vault."
   where
diff --git a/Remote/Helper/AWS.hs b/Remote/Helper/AWS.hs
--- a/Remote/Helper/AWS.hs
+++ b/Remote/Helper/AWS.hs
@@ -12,6 +12,8 @@
 
 import Annex.Common
 import Creds
+import Types.ProposedAccepted
+import Types.RemoteConfig
 
 import qualified Data.Map as M
 import qualified Data.ByteString as B
@@ -23,8 +25,11 @@
 creds u = CredPairStorage
 	{ credPairFile = fromUUID u
 	, credPairEnvironment = ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
-	, credPairRemoteField = "s3creds"
+	, credPairRemoteField = s3credsField
 	}
+
+s3credsField :: RemoteConfigField
+s3credsField = Accepted "s3creds"
 
 data Service = S3 | Glacier
 	deriving (Eq)
diff --git a/Remote/Helper/Chunked.hs b/Remote/Helper/Chunked.hs
--- a/Remote/Helper/Chunked.hs
+++ b/Remote/Helper/Chunked.hs
@@ -1,6 +1,6 @@
 {- git-annex chunked remotes
  -
- - Copyright 2014 Joey Hess <id@joeyh.name>
+ - Copyright 2014-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -10,17 +10,20 @@
 	ChunkConfig(..),
 	noChunks,
 	describeChunkConfig,
+	chunkConfigParsers,
 	getChunkConfig,
 	storeChunks,
 	removeChunks,
 	retrieveChunks,
 	checkPresentChunks,
+	chunkField,
 ) where
 
 import Annex.Common
 import Utility.DataUnits
 import Types.StoreRetrieve
 import Types.Remote
+import Types.ProposedAccepted
 import Logs.Chunk
 import Utility.Metered
 import Crypto (EncKey)
@@ -28,7 +31,6 @@
 import Annex.SpecialRemote.Config
 
 import qualified Data.ByteString.Lazy as L
-import qualified Data.Map as M
 
 data ChunkConfig
 	= NoChunks
@@ -48,19 +50,26 @@
 noChunks NoChunks = True
 noChunks _ = False
 
-getChunkConfig :: RemoteConfig -> ChunkConfig
-getChunkConfig m =
-	case M.lookup chunksizeField m of
-		Nothing -> case M.lookup "chunk" m of
+chunkConfigParsers :: [RemoteConfigFieldParser]
+chunkConfigParsers =
+	[ optionalStringParser chunksizeField HiddenField -- deprecated
+	, optionalStringParser chunkField
+		(FieldDesc "size of chunks (eg, 1MiB)")
+	]
+
+getChunkConfig :: ParsedRemoteConfig -> ChunkConfig
+getChunkConfig c =
+	case getRemoteConfigValue chunksizeField c of
+		Nothing -> case getRemoteConfigValue chunkField c of
 			Nothing -> NoChunks
-			Just v -> readsz UnpaddedChunks v "chunk"
+			Just v -> readsz UnpaddedChunks v chunkField
 		Just v -> readsz LegacyChunks v chunksizeField
   where
-	readsz c v f = case readSize dataUnits v of
+	readsz mk v f = case readSize dataUnits v of
 		Just size
 			| size == 0 -> NoChunks
-			| size > 0 -> c (fromInteger size)
-		_ -> giveup $ "bad configuration " ++ f ++ "=" ++ v
+			| size > 0 -> mk (fromInteger size)
+		_ -> giveup $ "bad configuration " ++ fromProposedAccepted f ++ "=" ++ v
 
 -- An infinite stream of chunk keys, starting from chunk 1.
 newtype ChunkKeyStream = ChunkKeyStream [Key]
diff --git a/Remote/Helper/Encryptable.hs b/Remote/Helper/Encryptable.hs
--- a/Remote/Helper/Encryptable.hs
+++ b/Remote/Helper/Encryptable.hs
@@ -1,15 +1,19 @@
 {- common functions for encryptable remotes
  -
- - Copyright 2011 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE FlexibleContexts, ScopedTypeVariables #-}
+
 module Remote.Helper.Encryptable (
 	EncryptionIsSetup,
 	encryptionSetup,
 	noEncryptionUsed,
 	encryptionAlreadySetup,
+	encryptionConfigParsers,
+	parseEncryptionConfig,
 	remoteCipher,
 	remoteCipher',
 	embedCreds,
@@ -17,17 +21,20 @@
 	extractCipher,
 	isEncrypted,
 	describeEncryption,
+	encryptionField,
+	highRandomQualityField
 ) where
 
 import qualified Data.Map as M
+import qualified Data.Set as S
 import qualified "sandi" Codec.Binary.Base64 as B64
 import qualified Data.ByteString as B
 
 import Annex.Common
 import Types.Remote
-import Config
 import Crypto
 import Types.Crypto
+import Types.ProposedAccepted
 import qualified Annex
 import Annex.SpecialRemote.Config
 
@@ -46,67 +53,170 @@
 encryptionAlreadySetup :: EncryptionIsSetup
 encryptionAlreadySetup = EncryptionIsSetup
 
+encryptionConfigParsers :: [RemoteConfigFieldParser]
+encryptionConfigParsers =
+	[ encryptionFieldParser
+	, optionalStringParser cipherField HiddenField
+	, optionalStringParser cipherkeysField HiddenField
+	, optionalStringParser pubkeysField HiddenField
+	, yesNoParser embedCredsField False
+		(FieldDesc "embed credentials into git repository")
+	, macFieldParser
+	, optionalStringParser (Accepted "keyid")
+		(FieldDesc "gpg key id")
+	, optionalStringParser (Accepted "keyid+")
+		(FieldDesc "add additional gpg key")
+	, optionalStringParser (Accepted "keyid-")
+		(FieldDesc "remove gpg key")
+	, highRandomQualityFieldParser
+	]
+
+encryptionConfigs :: S.Set RemoteConfigField
+encryptionConfigs = S.fromList (map parserForField encryptionConfigParsers)
+
+-- Parse only encryption fields, ignoring all others.
+parseEncryptionConfig :: RemoteConfig -> Either String ParsedRemoteConfig
+parseEncryptionConfig c = parseRemoteConfig
+	(M.restrictKeys c encryptionConfigs)
+	(RemoteConfigParser encryptionConfigParsers Nothing)
+
+encryptionFieldParser :: RemoteConfigFieldParser
+encryptionFieldParser = RemoteConfigFieldParser
+	{ parserForField = encryptionField
+	, valueParser = \v c -> Just . RemoteConfigValue
+		<$> parseEncryptionMethod (fmap fromProposedAccepted v) c
+	, fieldDesc = FieldDesc "how to encrypt data stored in the special remote"
+	, valueDesc = Just $ ValueDesc $
+		intercalate " or " (M.keys encryptionMethods)
+	}
+
+encryptionMethods :: M.Map String EncryptionMethod
+encryptionMethods = M.fromList
+	[ ("none", NoneEncryption)
+	, ("shared", SharedEncryption)
+	, ("hybrid", HybridEncryption)
+	, ("pubkey", PubKeyEncryption)
+	, ("sharedpubkey", SharedPubKeyEncryption)
+	]
+
+parseEncryptionMethod :: Maybe String -> RemoteConfig -> Either String EncryptionMethod
+parseEncryptionMethod (Just s) _ = case M.lookup s encryptionMethods of
+	Just em -> Right em
+	Nothing -> Left badEncryptionMethod
+-- Hybrid encryption is the default when a keyid is specified without
+-- an encryption field, or when there's a cipher already but no encryption
+-- field.
+parseEncryptionMethod Nothing c
+	| M.member (Accepted "keyid") c || M.member cipherField c = Right HybridEncryption
+	| otherwise = Left badEncryptionMethod
+
+badEncryptionMethod :: String
+badEncryptionMethod = "Specify " ++ intercalate " or "
+	(map ((fromProposedAccepted encryptionField ++ "=") ++)
+		(M.keys encryptionMethods))
+	++ "."
+
+highRandomQualityField :: RemoteConfigField
+highRandomQualityField = Accepted "highRandomQuality"
+
+highRandomQualityFieldParser :: RemoteConfigFieldParser
+highRandomQualityFieldParser = RemoteConfigFieldParser
+	{ parserForField = highRandomQualityField
+	, valueParser = \v _c -> Just . RemoteConfigValue
+		<$> parseHighRandomQuality (fmap fromProposedAccepted v)
+	, fieldDesc = HiddenField
+	, valueDesc = Nothing
+	}
+ 
+parseHighRandomQuality :: Maybe String -> Either String Bool
+parseHighRandomQuality Nothing = Right True
+parseHighRandomQuality (Just "false") = Right False
+parseHighRandomQuality (Just "true") = Right True
+parseHighRandomQuality _ = Left "expected highRandomQuality=true/false"
+	
+macFieldParser :: RemoteConfigFieldParser
+macFieldParser = RemoteConfigFieldParser
+	{ parserForField = macField
+	, valueParser = \v _c -> Just . RemoteConfigValue <$> parseMac v
+	, fieldDesc = FieldDesc "how to encrypt filenames used on the remote"
+	, valueDesc = Just $ ValueDesc $
+		intercalate " or " (M.keys macMap)
+	}
+
+parseMac :: Maybe (ProposedAccepted String) -> Either String Mac
+parseMac Nothing = Right defaultMac
+parseMac (Just (Accepted s)) = Right $ fromMaybe defaultMac (readMac s)
+parseMac (Just (Proposed s)) = case readMac s of
+	Just mac -> Right mac
+	Nothing -> Left "bad mac value"
+
 {- Encryption setup for a remote. The user must specify whether to use
  - an encryption key, or not encrypt. An encrypted cipher is created, or is
  - updated to be accessible to an additional encryption key. Or the user
  - could opt to use a shared cipher, which is stored unencrypted. -}
 encryptionSetup :: RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, EncryptionIsSetup)
 encryptionSetup c gc = do
+	pc <- either giveup return $ parseEncryptionConfig c
 	cmd <- gpgCmd <$> Annex.getGitConfig
-	maybe (genCipher cmd) (updateCipher cmd) (extractCipher c)
+	maybe (genCipher pc cmd) (updateCipher pc cmd) (extractCipher pc)
   where
 	-- The type of encryption
-	encryption = M.lookup encryptionField c
+	encryption = parseEncryptionMethod (fromProposedAccepted <$> M.lookup encryptionField c) c
 	-- Generate a new cipher, depending on the chosen encryption scheme
-	genCipher cmd = case encryption of
-		_ | hasEncryptionConfig c -> cannotchange
-		Just "none" -> return (c, NoEncryption)
-		Just "shared" -> encsetup $ genSharedCipher cmd
-		-- hybrid encryption is the default when a keyid is
-		-- specified but no encryption
-		_ | maybe (M.member "keyid" c) (== "hybrid") encryption ->
-			encsetup $ genEncryptedCipher cmd (c, gc) key Hybrid
-		Just "pubkey" -> encsetup $ genEncryptedCipher cmd (c, gc) key PubKey
-		Just "sharedpubkey" -> encsetup $ genSharedPubKeyCipher cmd key
-		_ -> giveup $ "Specify " ++ intercalate " or "
-			(map ((encryptionField ++ "=") ++)
-				["none","shared","hybrid","pubkey", "sharedpubkey"])
-			++ "."
-	key = fromMaybe (giveup "Specify keyid=...") $ M.lookup "keyid" c
-	newkeys = maybe [] (\k -> [(True,k)]) (M.lookup "keyid+" c) ++
-		maybe [] (\k -> [(False,k)]) (M.lookup "keyid-" c)
+	genCipher pc cmd = case encryption of
+		Right NoneEncryption -> return (c, NoEncryption)
+		Right SharedEncryption -> encsetup $ genSharedCipher cmd
+		Right HybridEncryption -> encsetup $ genEncryptedCipher cmd (pc, gc) key Hybrid
+		Right PubKeyEncryption -> encsetup $ genEncryptedCipher cmd (pc, gc) key PubKey
+		Right SharedPubKeyEncryption -> encsetup $ genSharedPubKeyCipher cmd key
+		Left err -> giveup err
+	key = maybe (giveup "Specify keyid=...") fromProposedAccepted $
+		M.lookup (Accepted "keyid") c
+	newkeys = maybe [] (\k -> [(True,fromProposedAccepted k)]) (M.lookup (Accepted "keyid+") c) ++
+		maybe [] (\k -> [(False,fromProposedAccepted k)]) (M.lookup (Accepted "keyid-") c)
 	cannotchange = giveup "Cannot set encryption type of existing remotes."
 	-- Update an existing cipher if possible.
-	updateCipher cmd v = case v of
-		SharedCipher _ | maybe True (== "shared") encryption -> return (c', EncryptionIsSetup)
-		EncryptedCipher _ variant _
-			| maybe True (== if variant == Hybrid then "hybrid" else "pubkey") encryption -> do
-				use "encryption update" $ updateCipherKeyIds cmd (c, gc) newkeys v
+	updateCipher pc cmd v = case v of
+		SharedCipher _ | encryption == Right SharedEncryption ->
+			return (c', EncryptionIsSetup)
+		EncryptedCipher _ variant _ | sameasencryption variant ->
+			use "encryption update" $ updateCipherKeyIds cmd (pc, gc) newkeys v
 		SharedPubKeyCipher _ _ ->
-			use "encryption update" $ updateCipherKeyIds cmd (c, gc) newkeys v
+			use "encryption update" $ updateCipherKeyIds cmd (pc, gc) newkeys v
 		_ -> cannotchange
+	sameasencryption variant = case encryption of
+		Right HybridEncryption -> variant == Hybrid
+		Right PubKeyEncryption -> variant == PubKey
+		Right _ -> False
+		Left _ -> True
 	encsetup a = use "encryption setup" . a =<< highRandomQuality
 	use m a = do
 		showNote m
 		cipher <- liftIO a
 		showNote (describeCipher cipher)
 		return (storeCipher cipher c', EncryptionIsSetup)
-	highRandomQuality = 
-		(&&) (maybe True ( /= "false") $ M.lookup "highRandomQuality" c)
-			<$> fmap not (Annex.getState Annex.fast)
+	highRandomQuality = ifM (Annex.getState Annex.fast)
+		( return False
+		, case parseHighRandomQuality (fromProposedAccepted <$> M.lookup highRandomQualityField c) of
+			Left err -> giveup err
+			Right v -> return v
+		)
 	c' = foldr M.delete c
-		-- git-annex used to remove 'encryption' as well, since
+		-- Remove configs that are only used in here to generate
+		-- the encryption keys, and should not be stored in
+		-- remote.log.
+		-- Older versions used to remove 'encryption' as well, since
 		-- it was redundant; we now need to keep it for
 		-- public-key encryption, hence we leave it on newer
 		-- remotes (while being backward-compatible).
-		[ "keyid", "keyid+", "keyid-", "highRandomQuality" ]
+		(map Accepted ["keyid", "keyid+", "keyid-", "highRandomQuality"])
 
-remoteCipher :: RemoteConfig -> RemoteGitConfig -> Annex (Maybe Cipher)
+remoteCipher :: ParsedRemoteConfig -> RemoteGitConfig -> Annex (Maybe Cipher)
 remoteCipher c gc = fmap fst <$> remoteCipher' c gc
 
 {- Gets encryption Cipher. The decrypted Ciphers are cached in the Annex
  - state. -}
-remoteCipher' :: RemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, StorableCipher))
+remoteCipher' :: ParsedRemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, StorableCipher))
 remoteCipher' c gc = go $ extractCipher c
   where
 	go Nothing = return Nothing
@@ -128,17 +238,19 @@
  - When gpg encryption is used and the creds are encrypted using it.
  - Not when a shared cipher is used.
  -}
-embedCreds :: RemoteConfig -> Bool
-embedCreds c = case yesNo =<< M.lookup "embedcreds" c of
+embedCreds :: ParsedRemoteConfig -> Bool
+embedCreds c = case getRemoteConfigValue embedCredsField c of
 	Just v -> v
-	Nothing -> isJust (M.lookup cipherkeysField c) && isJust (M.lookup cipherField c)
+	Nothing -> case (getRemoteConfigValue cipherkeysField c, getRemoteConfigValue cipherField c) of
+		(Just (_ :: ProposedAccepted String), Just (_ :: ProposedAccepted String)) -> True
+		_ -> False
 
 {- Gets encryption Cipher, and key encryptor. -}
-cipherKey :: RemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, EncKey))
+cipherKey :: ParsedRemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, EncKey))
 cipherKey c gc = fmap make <$> remoteCipher c gc
   where
 	make ciphertext = (ciphertext, encryptKey mac ciphertext)
-	mac = fromMaybe defaultMac $ M.lookup macField c >>= readMac
+	mac = fromMaybe defaultMac $ getRemoteConfigValue macField c
 
 {- Stores an StorableCipher in a remote's configuration. -}
 storeCipher :: StorableCipher -> RemoteConfig -> RemoteConfig
@@ -147,40 +259,32 @@
 	(EncryptedCipher t _ ks) -> addcipher t . storekeys ks cipherkeysField
 	(SharedPubKeyCipher t ks) -> addcipher t . storekeys ks pubkeysField
   where
-	addcipher t = M.insert cipherField (toB64bs t)
-	storekeys (KeyIds l) n = M.insert n (intercalate "," l)
+	addcipher t = M.insert cipherField (Accepted (toB64bs t))
+	storekeys (KeyIds l) n = M.insert n (Accepted (intercalate "," l))
 
 {- Extracts an StorableCipher from a remote's configuration. -}
-extractCipher :: RemoteConfig -> Maybe StorableCipher
-extractCipher c = case (M.lookup cipherField c,
-			M.lookup cipherkeysField c <|> M.lookup pubkeysField c,
-			M.lookup encryptionField c) of
-	(Just t, Just ks, encryption) | maybe True (== "hybrid") encryption ->
+extractCipher :: ParsedRemoteConfig -> Maybe StorableCipher
+extractCipher c = case (getRemoteConfigValue cipherField c,
+			(getRemoteConfigValue cipherkeysField c <|> getRemoteConfigValue pubkeysField c),
+			getRemoteConfigValue encryptionField c) of
+	(Just t, Just ks, Just HybridEncryption) ->
 		Just $ EncryptedCipher (fromB64bs t) Hybrid (readkeys ks)
-	(Just t, Just ks, Just "pubkey") ->
+	(Just t, Just ks, Just PubKeyEncryption) ->
 		Just $ EncryptedCipher (fromB64bs t) PubKey (readkeys ks)
-	(Just t, Just ks, Just "sharedpubkey") ->
+	(Just t, Just ks, Just SharedPubKeyEncryption) ->
 		Just $ SharedPubKeyCipher (fromB64bs t) (readkeys ks)
-	(Just t, Nothing, encryption) | maybe True (== "shared") encryption ->
+	(Just t, Nothing, Just SharedEncryption) ->
 		Just $ SharedCipher (fromB64bs t)
 	_ -> Nothing
   where
 	readkeys = KeyIds . splitc ','
 
-isEncrypted :: RemoteConfig -> Bool
-isEncrypted c = case M.lookup encryptionField c of
-	Just "none" -> False
-	Just _ -> True
-	Nothing -> hasEncryptionConfig c
-
-hasEncryptionConfig :: RemoteConfig -> Bool
-hasEncryptionConfig c = M.member cipherField c
-	|| M.member cipherkeysField c
-	|| M.member pubkeysField c
+isEncrypted :: ParsedRemoteConfig -> Bool
+isEncrypted = isJust . extractCipher
 
-describeEncryption :: RemoteConfig -> String
+describeEncryption :: ParsedRemoteConfig -> String
 describeEncryption c = case extractCipher c of
-	Nothing -> "none"
+	Nothing -> "none" ++ show (getRemoteConfigValue cipherField c :: Maybe String) ++ show (M.keys c)
 	Just cip -> nameCipher cip ++ " (" ++ describeCipher cip ++ ")"
 
 nameCipher :: StorableCipher -> String
diff --git a/Remote/Helper/ExportImport.hs b/Remote/Helper/ExportImport.hs
--- a/Remote/Helper/ExportImport.hs
+++ b/Remote/Helper/ExportImport.hs
@@ -13,25 +13,25 @@
 import Types.Remote
 import Types.Backend
 import Types.Key
+import Types.ProposedAccepted
 import Backend
 import Remote.Helper.Encryptable (isEncrypted)
 import qualified Database.Export as Export
 import qualified Database.ContentIdentifier as ContentIdentifier
 import Annex.Export
 import Annex.LockFile
-import Config
+import Annex.SpecialRemote.Config
 import Git.Types (fromRef)
 import Logs.Export
 import Logs.ContentIdentifier (recordContentIdentifier)
 
-import qualified Data.Map as M
 import Control.Concurrent.STM
 
 -- | Use for remotes that do not support exports.
 class HasExportUnsupported a where
 	exportUnsupported :: a
 
-instance HasExportUnsupported (RemoteConfig -> RemoteGitConfig -> Annex Bool) where
+instance HasExportUnsupported (ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool) where
 	exportUnsupported = \_ _ -> return False
 
 instance HasExportUnsupported (ExportActions Annex) where
@@ -50,7 +50,7 @@
 class HasImportUnsupported a where
 	importUnsupported :: a
 
-instance HasImportUnsupported (RemoteConfig -> RemoteGitConfig -> Annex Bool) where
+instance HasImportUnsupported (ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool) where
 	importUnsupported = \_ _ -> return False
 
 instance HasImportUnsupported (ImportActions Annex) where
@@ -63,59 +63,70 @@
 		, checkPresentExportWithContentIdentifier = \_ _ _ -> return False
 		}
 
-exportIsSupported :: RemoteConfig -> RemoteGitConfig -> Annex Bool
+exportIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
 exportIsSupported = \_ _ -> return True
 
-importIsSupported :: RemoteConfig -> RemoteGitConfig -> Annex Bool
+importIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
 importIsSupported = \_ _ -> return True
 
 -- | Prevent or allow exporttree=yes and importtree=yes when
 -- setting up a new remote, depending on exportSupported and importSupported.
 adjustExportImportRemoteType :: RemoteType -> RemoteType
-adjustExportImportRemoteType rt = rt { setup = setup' }
+adjustExportImportRemoteType rt = rt
+	{ setup = setup'
+	, configParser = configparser
+	}
   where
-	setup' st mu cp c gc =
-		let checkconfig supported configured setting cont =
-			ifM (supported rt c gc)
+	configparser c = addRemoteConfigParser exportImportConfigParsers 
+		<$> configParser rt c
+	setup' st mu cp c gc = do
+		pc <- either giveup return . parseRemoteConfig c =<< configparser c
+		let checkconfig supported configured configfield cont =
+			ifM (supported rt pc gc)
 				( case st of
 					Init
-						| configured c && isEncrypted c ->
-							giveup $ "cannot enable both encryption and " ++ setting
-						| otherwise -> cont
-					Enable oldc
-						| configured c /= configured oldc ->
-							giveup $ "cannot change " ++ setting ++ " of existing special remote"
+						| configured pc && isEncrypted pc ->
+							giveup $ "cannot enable both encryption and " ++ fromProposedAccepted configfield
 						| otherwise -> cont
-				, if configured c
-					then giveup $ setting ++ " is not supported by this special remote"
+					Enable oldc -> do
+						oldpc <- either mempty id . parseRemoteConfig oldc <$> configparser oldc
+						if configured pc /= configured oldpc
+							then giveup $ "cannot change " ++ fromProposedAccepted configfield ++ " of existing special remote"
+							else cont
+				, if configured pc
+					then giveup $ fromProposedAccepted configfield ++ " is not supported by this special remote"
 					else cont
 				)
-		in checkconfig exportSupported exportTree "exporttree" $
-			checkconfig importSupported importTree "importtree" $
-				if importTree c && not (exportTree c)
+		checkconfig exportSupported exportTree exportTreeField $
+			checkconfig importSupported importTree importTreeField $
+				if importTree pc && not (exportTree pc)
 					then giveup "cannot enable importtree=yes without also enabling exporttree=yes"
 					else setup rt st mu cp c gc
 
+exportImportConfigParsers :: [RemoteConfigFieldParser]
+exportImportConfigParsers =
+	[ yesNoParser exportTreeField False
+		(FieldDesc "export trees of files to this remote")
+	, yesNoParser importTreeField False
+		(FieldDesc "import trees of files from this remote")
+	]
+
 -- | Adjust a remote to support exporttree=yes and importree=yes.
 --
 -- Note that all remotes with importree=yes also have exporttree=yes.
 adjustExportImport :: Remote -> RemoteStateHandle -> Annex Remote
-adjustExportImport r rs = case M.lookup "exporttree" (config r) of
+adjustExportImport r rs = case getRemoteConfigValue exportTreeField (config r) of
 	Nothing -> return $ notexport r
-	Just c -> case yesNo c of
-		Just True -> ifM (isExportSupported r)
-			( do
-				exportdbv <- prepexportdb
-				r' <- isexport exportdbv
-				if importTree (config r)
-					then isimport r' exportdbv
-					else return r'
-			, return $ notexport r
-			)
-		Just False -> return $ notexport r
-		Nothing -> do
-			warning $ "bad exporttree value for " ++ name r ++ ", assuming not an export"
-			return $ notexport r
+	Just True -> ifM (isExportSupported r)
+		( do
+			exportdbv <- prepexportdb
+			r' <- isexport exportdbv
+			if importTree (config r)
+				then isimport r' exportdbv
+				else return r'
+		, return $ notexport r
+		)
+	Just False -> return $ notexport r
   where
 	notexport r' = notimport r'
 		{ exportActions = exportUnsupported
diff --git a/Remote/Helper/Special.hs b/Remote/Helper/Special.hs
--- a/Remote/Helper/Special.hs
+++ b/Remote/Helper/Special.hs
@@ -1,6 +1,6 @@
 {- helpers for special remotes
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -30,6 +30,8 @@
 	checkPresentDummy,
 	SpecialRemoteCfg(..),
 	specialRemoteCfg,
+	specialRemoteConfigParsers,
+	specialRemoteType,
 	specialRemote,
 	specialRemote',
 	lookupName,
@@ -149,7 +151,7 @@
 checkPresentDummy _ = error "missing checkPresent implementation"
 
 type RemoteModifier
-	= RemoteConfig
+	= ParsedRemoteConfig
 	-> Preparer Storer
 	-> Preparer Retriever
 	-> Preparer Remover
@@ -162,9 +164,19 @@
 	, displayProgress :: Bool
 	}
 
-specialRemoteCfg :: RemoteConfig -> SpecialRemoteCfg
+specialRemoteCfg :: ParsedRemoteConfig -> SpecialRemoteCfg
 specialRemoteCfg c = SpecialRemoteCfg (getChunkConfig c) True
 
+-- Modifies a base RemoteType to support chunking and encryption configs.
+specialRemoteType :: RemoteType -> RemoteType
+specialRemoteType r = r 
+	{ configParser = \c -> addRemoteConfigParser specialRemoteConfigParsers
+		<$> configParser r c
+	}
+
+specialRemoteConfigParsers :: [RemoteConfigFieldParser]
+specialRemoteConfigParsers = chunkConfigParsers ++ encryptionConfigParsers
+
 -- Modifies a base Remote to support both chunking and encryption,
 -- which special remotes typically should support.
 -- 
@@ -212,7 +224,7 @@
 			}
 		}
 	cip = cipherKey c (gitconfig baser)
-	isencrypted = isJust (extractCipher c)
+	isencrypted = isEncrypted c
 
 	safely a = catchNonAsync a (\e -> warning (show e) >> return False)
 
diff --git a/Remote/Hook.hs b/Remote/Hook.hs
--- a/Remote/Hook.hs
+++ b/Remote/Hook.hs
@@ -1,6 +1,6 @@
 {- A remote that provides hooks to run shell commands.
  -
- - Copyright 2011 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -15,11 +15,13 @@
 import Config
 import Config.Cost
 import Annex.UUID
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.Messages
 import Remote.Helper.ExportImport
 import Utility.Env
 import Messages.Progress
+import Types.ProposedAccepted
 
 import qualified Data.Map as M
 
@@ -27,16 +29,23 @@
 type HookName = String
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "hook"
 	, enumerate = const (findSpecialRemotes "hooktype")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser hooktypeField
+			(FieldDesc "(required) specify collection of hooks to use")
+		]
 	, setup = hookSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+hooktypeField :: RemoteConfigField
+hooktypeField = Accepted "hooktype"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	cst <- remoteCost gc expensiveRemoteCost
 	return $ Just $ specialRemote c
@@ -85,8 +94,8 @@
 hookSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
 hookSetup _ mu _ c gc = do
 	u <- maybe (liftIO genUUID) return mu
-	let hooktype = fromMaybe (giveup "Specify hooktype=") $
-		M.lookup "hooktype" c
+	let hooktype = maybe (giveup "Specify hooktype=") fromProposedAccepted $
+		M.lookup hooktypeField c
 	(c', _encsetup) <- encryptionSetup c gc
 	gitConfigSpecialRemote u c' [("hooktype", hooktype)]
 	return (c', u)
diff --git a/Remote/List.hs b/Remote/List.hs
--- a/Remote/List.hs
+++ b/Remote/List.hs
@@ -1,6 +1,6 @@
 {- git-annex remote list
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -20,6 +20,7 @@
 import Remote.Helper.Hooks
 import Remote.Helper.ReadOnly
 import Remote.Helper.ExportImport
+import Annex.SpecialRemote.Config
 import qualified Git
 import qualified Git.Config
 
@@ -109,7 +110,8 @@
 	let cu = fromMaybe u $ remoteAnnexConfigUUID gc
 	let rs = RemoteStateHandle cu
 	let c = fromMaybe M.empty $ M.lookup cu m
-	generate t g u c gc rs >>= \case
+	pc <- either (const mempty) id . parseRemoteConfig c <$> configParser t c
+	generate t g u pc gc rs >>= \case
 		Nothing -> return Nothing
 		Just r -> Just <$> adjustExportImport (adjustReadOnly (addHooks r)) rs
 
diff --git a/Remote/P2P.hs b/Remote/P2P.hs
--- a/Remote/P2P.hs
+++ b/Remote/P2P.hs
@@ -36,12 +36,13 @@
 	-- and will call chainGen on them.
 	, enumerate = const (return [])
 	, generate = \_ _ _ _ _ -> return Nothing
+	, configParser = mkRemoteConfigParser []
 	, setup = error "P2P remotes are set up using git-annex p2p"
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-chainGen :: P2PAddress -> Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+chainGen :: P2PAddress -> Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 chainGen addr r u c gc rs = do
 	connpool <- mkConnectionPool
 	cst <- remoteCost gc veryExpensiveRemoteCost
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -1,6 +1,6 @@
 {- A remote that is only accessible by rsync.
  -
- - Copyright 2011-2018 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -14,6 +14,7 @@
 	remove,
 	checkKey,
 	withRsyncScratchDir,
+	rsyncRemoteConfigs,
 	genRsyncOpts,
 	RsyncOpts
 ) where
@@ -30,6 +31,7 @@
 import Remote.Helper.Messages
 import Remote.Helper.ExportImport
 import Types.Export
+import Types.ProposedAccepted
 import Remote.Rsync.RsyncUrl
 import Crypto
 import Utility.Rsync
@@ -41,20 +43,31 @@
 import Annex.DirHashes
 import Utility.Tmp.Dir
 import Utility.SshHost
+import Annex.SpecialRemote.Config
 
 import qualified Data.Map as M
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "rsync"
 	, enumerate = const (findSpecialRemotes "rsyncurl")
 	, generate = gen
+	, configParser = mkRemoteConfigParser $ rsyncRemoteConfigs ++
+		[ optionalStringParser rsyncUrlField
+			(FieldDesc "(required) url or hostname:/directory for rsync to use")
+		]
 	, setup = rsyncSetup
 	, exportSupported = exportIsSupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+shellEscapeField :: RemoteConfigField
+shellEscapeField = Accepted "shellescape"
+
+rsyncUrlField :: RemoteConfigField
+rsyncUrlField = Accepted "rsyncurl"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	cst <- remoteCost gc expensiveRemoteCost
 	(transport, url) <- rsyncTransport gc $
@@ -111,7 +124,14 @@
 		-- Rsync displays its own progress.
 		{ displayProgress = False }
 
-genRsyncOpts :: RemoteConfig -> RemoteGitConfig -> Annex [CommandParam] -> RsyncUrl -> RsyncOpts
+-- Things used by genRsyncOpts
+rsyncRemoteConfigs :: [RemoteConfigFieldParser]
+rsyncRemoteConfigs = 
+	[ yesNoParser shellEscapeField True
+		(FieldDesc "avoid usual shell escaping (not recommended)")
+	]
+
+genRsyncOpts :: ParsedRemoteConfig -> RemoteGitConfig -> Annex [CommandParam] -> RsyncUrl -> RsyncOpts
 genRsyncOpts c gc transport url = RsyncOpts
 	{ rsyncUrl = url
 	, rsyncOptions = appendtransport $ opts []
@@ -119,7 +139,7 @@
 		opts (remoteAnnexRsyncUploadOptions gc)
 	, rsyncDownloadOptions = appendtransport $
 		opts (remoteAnnexRsyncDownloadOptions gc)
-	, rsyncShellEscape = (yesNo =<< M.lookup "shellescape" c) /= Just False
+	, rsyncShellEscape = fromMaybe True (getRemoteConfigValue shellEscapeField c)
 	}
   where
 	appendtransport l = (++ l) <$> transport
@@ -161,8 +181,8 @@
 rsyncSetup _ mu _ c gc = do
 	u <- maybe (liftIO genUUID) return mu
 	-- verify configuration is sane
-	let url = fromMaybe (giveup "Specify rsyncurl=") $
-		M.lookup "rsyncurl" c
+	let url = maybe (giveup "Specify rsyncurl=") fromProposedAccepted $
+		M.lookup rsyncUrlField c
 	(c', _encsetup) <- encryptionSetup c gc
 
 	-- The rsyncurl is stored in git config, not only in this remote's
diff --git a/Remote/Rsync/RsyncUrl.hs b/Remote/Rsync/RsyncUrl.hs
--- a/Remote/Rsync/RsyncUrl.hs
+++ b/Remote/Rsync/RsyncUrl.hs
@@ -43,10 +43,10 @@
 rsyncUrls :: RsyncOpts -> Key -> [RsyncUrl]
 rsyncUrls o k = map use dirHashes
   where
-	use h = rsyncUrl o </> fromRawFilePath (hash h) </> rsyncEscape o (f </> f)
+	use h = rsyncUrl o </> hash h </> rsyncEscape o (f </> f)
 	f = fromRawFilePath (keyFile k)
 #ifndef mingw32_HOST_OS
-	hash h = h def k
+	hash h = fromRawFilePath $ h def k
 #else
-	hash h = replace "\\" "/" (h def k)
+	hash h = replace "\\" "/" $ fromRawFilePath $ h def k
 #endif
diff --git a/Remote/S3.hs b/Remote/S3.hs
--- a/Remote/S3.hs
+++ b/Remote/S3.hs
@@ -1,6 +1,6 @@
 {- S3 remotes
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -57,6 +57,7 @@
 import Logs.Web
 import Logs.MetaData
 import Types.MetaData
+import Types.ProposedAccepted
 import Utility.Metered
 import Utility.DataUnits
 import Annex.Content
@@ -68,16 +69,92 @@
 type BucketObject = String
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "S3"
 	, enumerate = const (findSpecialRemotes "s3")
 	, generate = gen
+	, configParser = const $ pure $ RemoteConfigParser
+		{ remoteConfigFieldParsers = 
+			[ optionalStringParser bucketField
+				(FieldDesc "name of bucket to store content in")
+			, optionalStringParser hostField
+				(FieldDesc "S3 server hostname (default is Amazon S3)")
+			, optionalStringParser datacenterField
+				(FieldDesc "S3 datacenter to use (US, EU, us-west-1, ..)")
+			, optionalStringParser partsizeField
+				(FieldDesc "part size for multipart upload (eg 1GiB)")
+			, optionalStringParser storageclassField
+				(FieldDesc "storage class, eg STANDARD or REDUCED_REDUNDANCY")
+			, optionalStringParser fileprefixField
+				(FieldDesc "prefix to add to filenames in the bucket")
+			, yesNoParser versioningField False
+				(FieldDesc "enable versioning of bucket content")
+			, yesNoParser publicField False
+				(FieldDesc "allow public read access to the buckey")
+			, optionalStringParser publicurlField
+				(FieldDesc "url that can be used by public to download files")
+			, optionalStringParser protocolField
+				(FieldDesc "http or https")
+			, optionalStringParser portField
+				(FieldDesc "port to connect to")
+			, optionalStringParser requeststyleField
+				(FieldDesc "for path-style requests, set to \"path\"")
+			, optionalStringParser mungekeysField HiddenField
+			, optionalStringParser AWS.s3credsField HiddenField
+			]
+		, remoteConfigRestPassthrough = Just
+			( \f -> isMetaHeader f || isArchiveMetaHeader f
+			,
+				[ ("x-amz-meta-*", FieldDesc "http headers to add when storing on S3")
+				, ("x-archive-meta-*", FieldDesc "http headers to add when storing on Internet Archive")
+			  	]
+			)
+		}
 	, setup = s3Setup
 	, exportSupported = exportIsSupported
 	, importSupported = importIsSupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+bucketField :: RemoteConfigField
+bucketField = Accepted "bucket"
+
+hostField :: RemoteConfigField
+hostField = Accepted "host"
+
+datacenterField :: RemoteConfigField
+datacenterField = Accepted "datacenter"
+
+partsizeField :: RemoteConfigField
+partsizeField = Accepted "partsize"
+
+storageclassField :: RemoteConfigField
+storageclassField = Accepted "storageclass"
+
+fileprefixField :: RemoteConfigField
+fileprefixField = Accepted "fileprefix"
+
+versioningField :: RemoteConfigField
+versioningField = Accepted "versioning"
+
+publicField :: RemoteConfigField
+publicField = Accepted "public"
+
+publicurlField :: RemoteConfigField
+publicurlField = Accepted "publicurl"
+
+protocolField :: RemoteConfigField
+protocolField = Accepted "protocol"
+
+requeststyleField :: RemoteConfigField
+requeststyleField = Accepted "requeststyle"
+
+portField :: RemoteConfigField
+portField = Accepted "port"
+
+mungekeysField :: RemoteConfigField
+mungekeysField = Accepted "mungekeys"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	cst <- remoteCost gc expensiveRemoteCost
 	info <- extractS3Info c
@@ -134,7 +211,7 @@
 			, appendonly = versioning info
 			, availability = GloballyAvailable
 			, remotetype = remote
-			, mkUnavailable = gen r u (M.insert "host" "!dne!" c) gc rs
+			, mkUnavailable = gen r u (M.insert hostField (RemoteConfigValue ("!dne!" :: String)) c) gc rs
 			, getInfo = includeCredsInfo c (AWS.creds u) (s3Info c info)
 			, claimUrl = Nothing
 			, checkUrl = Nothing
@@ -148,21 +225,21 @@
 
 s3Setup' :: SetupStage -> UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
 s3Setup' ss u mcreds c gc
-	| configIA c = archiveorg
+	| maybe False (isIAHost . fromProposedAccepted) (M.lookup hostField c) = archiveorg
 	| otherwise = defaulthost
   where
 	remotename = fromJust (lookupName c)
 	defbucket = remotename ++ "-" ++ fromUUID u
 	defaults = M.fromList
-		[ ("datacenter", T.unpack $ AWS.defaultRegion AWS.S3)
-		, ("storageclass", "STANDARD")
-		, ("host", AWS.s3DefaultHost)
-		, ("port", "80")
-		, ("bucket", defbucket)
+		[ (datacenterField, Proposed $ T.unpack $ AWS.defaultRegion AWS.S3)
+		, (storageclassField, Proposed "STANDARD")
+		, (hostField, Proposed AWS.s3DefaultHost)
+		, (portField, Proposed "80")
+		, (bucketField, Proposed defbucket)
 		]
-		
-	use fullconfig info = do
-		enableBucketVersioning ss info fullconfig gc u
+
+	use fullconfig pc info = do
+		enableBucketVersioning ss info pc gc u
 		gitConfigSpecialRemote u fullconfig [("s3", "true")]
 		return (fullconfig, u)
 
@@ -170,36 +247,40 @@
 		(c', encsetup) <- encryptionSetup c gc
 		c'' <- setRemoteCredPair encsetup c' gc (AWS.creds u) mcreds
 		let fullconfig = c'' `M.union` defaults
-		info <- extractS3Info fullconfig
-		checkexportimportsafe fullconfig info
+		pc <- either giveup return . parseRemoteConfig fullconfig
+			=<< configParser remote fullconfig
+		info <- extractS3Info pc
+		checkexportimportsafe pc info
 		case ss of
-			Init -> genBucket fullconfig gc u
+			Init -> genBucket pc gc u
 			_ -> return ()
-		use fullconfig info
+		use fullconfig pc info
 
 	archiveorg = do
 		showNote "Internet Archive mode"
 		c' <- setRemoteCredPair noEncryptionUsed c gc (AWS.creds u) mcreds
 		-- Ensure user enters a valid bucket name, since
 		-- this determines the name of the archive.org item.
-		let validbucket = replace " " "-" $
-			fromMaybe (giveup "specify bucket=") $
-				getBucketName c'
+		let validbucket = replace " " "-" $ map toLower $
+			maybe (giveup "specify bucket=") fromProposedAccepted
+				(M.lookup bucketField c')
 		let archiveconfig = 
 			-- IA acdepts x-amz-* as an alias for x-archive-*
-			M.mapKeys (replace "x-archive-" "x-amz-") $
+			M.mapKeys (Proposed . replace "x-archive-" "x-amz-" . fromProposedAccepted) $
 			-- encryption does not make sense here
-			M.insert encryptionField "none" $
-			M.insert "bucket" validbucket $
+			M.insert encryptionField (Proposed "none") $
+			M.insert bucketField (Proposed validbucket) $
 			M.union c' $
 			-- special constraints on key names
-			M.insert "mungekeys" "ia" defaults
-		info <- extractS3Info archiveconfig
-		checkexportimportsafe archiveconfig info
-		hdl <- mkS3HandleVar archiveconfig gc u
+			M.insert mungekeysField (Proposed "ia") defaults
+		pc <- either giveup return . parseRemoteConfig archiveconfig
+			=<< configParser remote archiveconfig
+		info <- extractS3Info pc
+		checkexportimportsafe pc info
+		hdl <- mkS3HandleVar pc gc u
 		withS3HandleOrFail u hdl $
-			writeUUIDFile archiveconfig u info
-		use archiveconfig info
+			writeUUIDFile pc u info
+		use archiveconfig pc info
 	
 	checkexportimportsafe c' info =
 		unlessM (Annex.getState Annex.force) $
@@ -293,7 +374,7 @@
 {- Implemented as a fileRetriever, that uses conduit to stream the chunks
  - out to the file. Would be better to implement a byteRetriever, but
  - that is difficult. -}
-retrieve :: S3HandleVar -> Remote -> RemoteStateHandle -> RemoteConfig -> S3Info -> Retriever
+retrieve :: S3HandleVar -> Remote -> RemoteStateHandle -> ParsedRemoteConfig -> S3Info -> Retriever
 retrieve hv r rs c info = fileRetriever $ \f k p -> withS3Handle hv $ \case
 	(Just h) -> 
 		eitherS3VersionID info rs c k (T.pack $ bucketObject info k) >>= \case
@@ -306,7 +387,7 @@
 			Left failreason -> do
 				warning failreason
 				giveup "cannot download content"
-			Right us -> unlessM (downloadUrl k p us f) $
+			Right us -> unlessM (withUrlOptions $ downloadUrl k p us f) $
 				giveup "failed to download content"
 
 retrieveHelper :: S3Info -> S3Handle -> (Either S3.Object S3VersionID) -> FilePath -> MeterUpdate -> Annex ()
@@ -330,7 +411,7 @@
 		S3.DeleteObject (T.pack $ bucketObject info k) (bucket info)
 	return $ either (const False) (const True) res
 
-checkKey :: S3HandleVar -> Remote -> RemoteStateHandle -> RemoteConfig -> S3Info -> CheckPresent
+checkKey :: S3HandleVar -> Remote -> RemoteStateHandle -> ParsedRemoteConfig -> S3Info -> CheckPresent
 checkKey hv r rs c info k = withS3Handle hv $ \case
 	Just h -> do
 		showChecking r
@@ -627,7 +708,7 @@
  - so first check if the UUID file already exists and we can skip creating
  - it.
  -}
-genBucket :: RemoteConfig -> RemoteGitConfig -> UUID -> Annex ()
+genBucket :: ParsedRemoteConfig -> RemoteGitConfig -> UUID -> Annex ()
 genBucket c gc u = do
 	showAction "checking bucket"
 	info <- extractS3Info c
@@ -652,7 +733,7 @@
 		writeUUIDFile c u info h
 	
 	locconstraint = mkLocationConstraint $ T.pack datacenter
-	datacenter = fromJust $ M.lookup "datacenter" c
+	datacenter = fromJust $ getRemoteConfigValue datacenterField c
 	-- "NEARLINE" as a storage class when creating a bucket is a
 	-- nonstandard extension of Google Cloud Storage.
 	storageclass = case getStorageClass c of
@@ -667,7 +748,7 @@
  - Note that IA buckets can only created by having a file
  - stored in them. So this also takes care of that.
  -}
-writeUUIDFile :: RemoteConfig -> UUID -> S3Info -> S3Handle -> Annex ()
+writeUUIDFile :: ParsedRemoteConfig -> UUID -> S3Info -> S3Handle -> Annex ()
 writeUUIDFile c u info h = do
 	v <- checkUUIDFile c u info h
 	case v of
@@ -684,7 +765,7 @@
 
 {- Checks if the UUID file exists in the bucket
  - and has the specified UUID already. -}
-checkUUIDFile :: RemoteConfig -> UUID -> S3Info -> S3Handle -> Annex (Either SomeException Bool)
+checkUUIDFile :: ParsedRemoteConfig -> UUID -> S3Info -> S3Handle -> Annex (Either SomeException Bool)
 checkUUIDFile c u info h = tryNonAsync $ liftIO $ runResourceT $ do
 	resp <- tryS3 $ sendS3Handle h (S3.getObject (bucket info) file)
 	case resp of
@@ -700,7 +781,7 @@
 	file = T.pack $ uuidFile c
 	uuidb = L.fromChunks [T.encodeUtf8 $ T.pack $ fromUUID u]
 
-uuidFile :: RemoteConfig -> FilePath
+uuidFile :: ParsedRemoteConfig -> FilePath
 uuidFile c = getFilePrefix c ++ "annex-uuid"
 
 tryS3 :: ResourceT IO a -> ResourceT IO (Either S3.S3Error a)
@@ -724,7 +805,7 @@
 
 {- Prepares a S3Handle for later use. Does not connect to S3 or do anything
  - else expensive. -}
-mkS3HandleVar :: RemoteConfig -> RemoteGitConfig -> UUID -> Annex S3HandleVar
+mkS3HandleVar :: ParsedRemoteConfig -> RemoteGitConfig -> UUID -> Annex S3HandleVar
 mkS3HandleVar c gc u = liftIO $ newTVarIO $ Left $ do
 	mcreds <- getRemoteCredPair c gc (AWS.creds u)
 	case mcreds of
@@ -755,24 +836,24 @@
 needS3Creds :: UUID -> String
 needS3Creds u = missingCredPairFor "S3" (AWS.creds u)
 
-s3Configuration :: RemoteConfig -> S3.S3Configuration AWS.NormalQuery
+s3Configuration :: ParsedRemoteConfig -> S3.S3Configuration AWS.NormalQuery
 s3Configuration c = cfg
 	{ S3.s3Port = port
-	, S3.s3RequestStyle = case M.lookup "requeststyle" c of
+	, S3.s3RequestStyle = case getRemoteConfigValue requeststyleField c of
 		Just "path" -> S3.PathStyle
 		Just s -> giveup $ "bad S3 requeststyle value: " ++ s
 		Nothing -> S3.s3RequestStyle cfg
 	}
   where
-	h = fromJust $ M.lookup "host" c
-	datacenter = fromJust $ M.lookup "datacenter" c
+	h = fromJust $ getRemoteConfigValue hostField c
+	datacenter = fromJust $ getRemoteConfigValue datacenterField c
 	-- When the default S3 host is configured, connect directly to
 	-- the S3 endpoint for the configured datacenter.
 	-- When another host is configured, it's used as-is.
 	endpoint
 		| h == AWS.s3DefaultHost = AWS.s3HostName $ T.pack datacenter
 		| otherwise = T.encodeUtf8 $ T.pack h
-	port = case M.lookup "port" c of
+	port = case getRemoteConfigValue portField c of
 		Just s -> 
 			case reads s of
 				[(p, _)]
@@ -787,7 +868,7 @@
 			Just AWS.HTTPS -> 443
 			Just AWS.HTTP -> 80
 			Nothing -> 80
-	cfgproto = case M.lookup "protocol" c of
+	cfgproto = case getRemoteConfigValue protocolField c of
 		Just "https" -> Just AWS.HTTPS
 		Just "http" -> Just AWS.HTTP
 		Just s -> giveup $ "bad S3 protocol value: " ++ s
@@ -814,7 +895,7 @@
 	, host :: Maybe String
 	}
 
-extractS3Info :: RemoteConfig -> Annex S3Info
+extractS3Info :: ParsedRemoteConfig -> Annex S3Info
 extractS3Info c = do
 	b <- maybe
 		(giveup "S3 bucket not configured")
@@ -829,13 +910,13 @@
 		, metaHeaders = getMetaHeaders c
 		, partSize = getPartSize c
 		, isIA = configIA c
-		, versioning = boolcfg "versioning"
-		, public = boolcfg "public"
-		, publicurl = M.lookup "publicurl" c
-		, host = M.lookup "host" c
+		, versioning = fromMaybe False $
+			getRemoteConfigValue versioningField c
+		, public = fromMaybe False $
+			getRemoteConfigValue publicField c
+		, publicurl = getRemoteConfigValue publicurlField c
+		, host = getRemoteConfigValue hostField c
 		}
-  where
-	boolcfg k = fromMaybe False $ yesNo =<< M.lookup k c
 
 putObject :: S3Info -> T.Text -> RequestBody -> S3.PutObject
 putObject info file rbody = (S3.putObject (bucket info) file rbody)
@@ -850,41 +931,51 @@
 	| public info = Just S3.AclPublicRead
 	| otherwise = Nothing
 
-getBucketName :: RemoteConfig -> Maybe BucketName
-getBucketName = map toLower <$$> M.lookup "bucket"
+getBucketName :: ParsedRemoteConfig -> Maybe BucketName
+getBucketName = map toLower <$$> getRemoteConfigValue bucketField
 
-getStorageClass :: RemoteConfig -> S3.StorageClass
-getStorageClass c = case M.lookup "storageclass" c of
+getStorageClass :: ParsedRemoteConfig -> S3.StorageClass
+getStorageClass c = case getRemoteConfigValue storageclassField c of
 	Just "REDUCED_REDUNDANCY" -> S3.ReducedRedundancy
 	Just s -> S3.OtherStorageClass (T.pack s)
 	_ -> S3.Standard
 
-getPartSize :: RemoteConfig -> Maybe Integer
-getPartSize c = readSize dataUnits =<< M.lookup "partsize" c
+getPartSize :: ParsedRemoteConfig -> Maybe Integer
+getPartSize c = readSize dataUnits =<< getRemoteConfigValue partsizeField c
 
-getMetaHeaders :: RemoteConfig -> [(T.Text, T.Text)]
-getMetaHeaders = map munge . filter ismetaheader . M.assocs
+getMetaHeaders :: ParsedRemoteConfig -> [(T.Text, T.Text)]
+getMetaHeaders = map munge
+	. filter (isMetaHeader . fst)
+	. M.assocs
+	. getRemoteConfigPassedThrough
   where
-	ismetaheader (h, _) = metaprefix `isPrefixOf` h
-	metaprefix = "x-amz-meta-"
-	metaprefixlen = length metaprefix
-	munge (k, v) = (T.pack $ drop metaprefixlen k, T.pack v)
+	metaprefixlen = length metaPrefix
+	munge (k, v) = (T.pack $ drop metaprefixlen (fromProposedAccepted k), T.pack v)
 
-getFilePrefix :: RemoteConfig -> String
-getFilePrefix = M.findWithDefault "" "fileprefix"
+isMetaHeader :: RemoteConfigField -> Bool
+isMetaHeader h = metaPrefix `isPrefixOf` fromProposedAccepted h
 
-getBucketObject :: RemoteConfig -> Key -> BucketObject
+isArchiveMetaHeader :: RemoteConfigField -> Bool
+isArchiveMetaHeader h = "x-archive-" `isPrefixOf` fromProposedAccepted h
+
+metaPrefix :: String
+metaPrefix = "x-amz-meta-"
+
+getFilePrefix :: ParsedRemoteConfig -> String
+getFilePrefix = fromMaybe "" . getRemoteConfigValue fileprefixField
+
+getBucketObject :: ParsedRemoteConfig -> Key -> BucketObject
 getBucketObject c = munge . serializeKey
   where
-	munge s = case M.lookup "mungekeys" c of
+	munge s = case getRemoteConfigValue mungekeysField c :: Maybe String of
 		Just "ia" -> iaMunge $ getFilePrefix c ++ s
 		_ -> getFilePrefix c ++ s
 
-getBucketExportLocation :: RemoteConfig -> ExportLocation -> BucketObject
+getBucketExportLocation :: ParsedRemoteConfig -> ExportLocation -> BucketObject
 getBucketExportLocation c loc =
 	getFilePrefix c ++ fromRawFilePath (fromExportLocation loc)
 
-getBucketImportLocation :: RemoteConfig -> BucketObject -> Maybe ImportLocation
+getBucketImportLocation :: ParsedRemoteConfig -> BucketObject -> Maybe ImportLocation
 getBucketImportLocation c obj
 	-- The uuidFile should not be imported.
 	| obj == uuidfile = Nothing
@@ -910,8 +1001,8 @@
 		| isSpace c = []
 		| otherwise = "&" ++ show (ord c) ++ ";"
 
-configIA :: RemoteConfig -> Bool
-configIA = maybe False isIAHost . M.lookup "host"
+configIA :: ParsedRemoteConfig -> Bool
+configIA = maybe False isIAHost . getRemoteConfigValue hostField
 
 {- Hostname to use for archive.org S3. -}
 iaHost :: HostName
@@ -963,7 +1054,7 @@
 		AWS.Warning -> warningM
 		AWS.Error -> errorM
 
-s3Info :: RemoteConfig -> S3Info -> [(String, String)]
+s3Info :: ParsedRemoteConfig -> S3Info -> [(String, String)]
 s3Info c info = catMaybes
 	[ Just ("bucket", fromMaybe "unknown" (getBucketName c))
 	, Just ("endpoint", w82s (BS.unpack (S3.s3Endpoint s3c)))
@@ -982,10 +1073,10 @@
 	showstorageclass (S3.OtherStorageClass t) = T.unpack t
 	showstorageclass sc = show sc
 
-getPublicWebUrls :: UUID -> RemoteStateHandle -> S3Info -> RemoteConfig -> Key -> Annex [URLString]
+getPublicWebUrls :: UUID -> RemoteStateHandle -> S3Info -> ParsedRemoteConfig -> Key -> Annex [URLString]
 getPublicWebUrls u rs info c k = either (const []) id <$> getPublicWebUrls' u rs info c k
 
-getPublicWebUrls' :: UUID -> RemoteStateHandle -> S3Info -> RemoteConfig -> Key -> Annex (Either String [URLString])
+getPublicWebUrls' :: UUID -> RemoteStateHandle -> S3Info -> ParsedRemoteConfig -> Key -> Annex (Either String [URLString])
 getPublicWebUrls' u rs info c k
 	| not (public info) = return $ Left $ 
 		"S3 bucket does not allow public access; " ++ needS3Creds u
@@ -1125,7 +1216,7 @@
 s3VersionField :: MetaField
 s3VersionField = mkMetaFieldUnchecked "V"
 
-eitherS3VersionID :: S3Info -> RemoteStateHandle -> RemoteConfig -> Key -> S3.Object -> Annex (Either String (Either S3.Object S3VersionID))
+eitherS3VersionID :: S3Info -> RemoteStateHandle -> ParsedRemoteConfig -> Key -> S3.Object -> Annex (Either String (Either S3.Object S3VersionID))
 eitherS3VersionID info rs c k fallback
 	| versioning info = getS3VersionID rs k >>= return . \case
 		[] -> if exportTree c
@@ -1150,7 +1241,7 @@
 -- Enable versioning on the bucket can only be done at init time;
 -- setting versioning in a bucket that git-annex has already exported
 -- files to risks losing the content of those un-versioned files.
-enableBucketVersioning :: SetupStage -> S3Info -> RemoteConfig -> RemoteGitConfig -> UUID -> Annex ()
+enableBucketVersioning :: SetupStage -> S3Info -> ParsedRemoteConfig -> RemoteGitConfig -> UUID -> Annex ()
 #if MIN_VERSION_aws(0,21,1)
 enableBucketVersioning ss info c gc u = do
 #else
@@ -1160,7 +1251,10 @@
 		Init -> when (versioning info) $
 			enableversioning (bucket info)
 		Enable oldc -> do
-			oldinfo <- extractS3Info oldc
+			oldpc <- either (const mempty) id
+				. parseRemoteConfig oldc 
+				<$> configParser remote oldc
+			oldinfo <- extractS3Info oldpc
 			when (versioning info /= versioning oldinfo) $
 				giveup "Cannot change versioning= of existing S3 remote."
   where
diff --git a/Remote/Tahoe.hs b/Remote/Tahoe.hs
--- a/Remote/Tahoe.hs
+++ b/Remote/Tahoe.hs
@@ -13,7 +13,7 @@
  -
  - Tahoe has its own encryption, so git-annex's encryption is not used.
  -
- - Copyright 2014 Joey Hess <id@joeyh.name>
+ - Copyright 2014-2019 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -30,9 +30,11 @@
 import Annex.Common
 import Types.Remote
 import Types.Creds
+import Types.ProposedAccepted
 import qualified Git
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.ExportImport
 import Annex.UUID
@@ -52,16 +54,27 @@
 type Capability = String
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "tahoe"
 	, enumerate = const (findSpecialRemotes "tahoe")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser scsField
+			(FieldDesc "optional, normally a unique one is generated")
+		, optionalStringParser furlField HiddenField
+		]
 	, setup = tahoeSetup
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+scsField :: RemoteConfigField
+scsField = Accepted "shared-convergence-secret"
+
+furlField :: RemoteConfigField
+furlField = Accepted "introducer-furl"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = do
 	cst <- remoteCost gc expensiveRemoteCost
 	hdl <- liftIO $ TahoeHandle
@@ -102,22 +115,23 @@
 
 tahoeSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
 tahoeSetup _ mu _ c _ = do
-	furl <- fromMaybe (fromMaybe missingfurl $ M.lookup furlk c)
+	furl <- maybe (fromMaybe missingfurl $ M.lookup furlField c) Proposed
 		<$> liftIO (getEnv "TAHOE_FURL")
 	u <- maybe (liftIO genUUID) return mu
 	configdir <- liftIO $ defaultTahoeConfigDir u
-	scs <- liftIO $ tahoeConfigure configdir furl (M.lookup scsk c)
-	let c' = if (yesNo =<< M.lookup "embedcreds" c) == Just True
+	scs <- liftIO $ tahoeConfigure configdir
+		(fromProposedAccepted furl)
+		(fromProposedAccepted <$> (M.lookup scsField c))
+	pc <- either giveup return . parseRemoteConfig c =<< configParser remote c
+	let c' = if embedCreds pc
 		then flip M.union c $ M.fromList
-			[ (furlk, furl)
-			, (scsk, scs)
+			[ (furlField, furl)
+			, (scsField, Proposed scs)
 			]
 		else c
 	gitConfigSpecialRemote u c' [("tahoe", configdir)]
 	return (c', u)
   where
-	scsk = "shared-convergence-secret"
-	furlk = "introducer-furl"
 	missingfurl = giveup "Set TAHOE_FURL to the introducer furl to use."
 
 store :: RemoteStateHandle -> TahoeHandle -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
diff --git a/Remote/Web.hs b/Remote/Web.hs
--- a/Remote/Web.hs
+++ b/Remote/Web.hs
@@ -27,6 +27,7 @@
 	{ typename = "web"
 	, enumerate = list
 	, generate = gen
+	, configParser = mkRemoteConfigParser []
 	, setup = error "not supported"
 	, exportSupported = exportUnsupported
 	, importSupported = importUnsupported
@@ -40,7 +41,7 @@
 	r <- liftIO $ Git.Construct.remoteNamed "web" (pure Git.Construct.fromUnknown)
 	return [r]
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r _ c gc rs = do
 	cst <- remoteCost gc expensiveRemoteCost
 	return $ Just Remote
@@ -89,7 +90,7 @@
 			YoutubeDownloader -> do
 				showOutput
 				youtubeDlTo key u' dest
-			_ -> downloadUrl key p [u'] dest
+			_ -> Url.withUrlOptions $ downloadUrl key p [u'] dest
 
 downloadKeyCheap :: Key -> AssociatedFile -> FilePath -> Annex Bool
 downloadKeyCheap _ _ _ = return False
diff --git a/Remote/WebDAV.hs b/Remote/WebDAV.hs
--- a/Remote/WebDAV.hs
+++ b/Remote/WebDAV.hs
@@ -1,6 +1,6 @@
 {- WebDAV remotes.
  -
- - Copyright 2012-2017 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -29,6 +29,7 @@
 import qualified Git
 import Config
 import Config.Cost
+import Annex.SpecialRemote.Config
 import Remote.Helper.Special
 import Remote.Helper.Messages
 import Remote.Helper.Http
@@ -39,18 +40,30 @@
 import Utility.Url (URLString, matchStatusCodeException, matchHttpExceptionContent)
 import Annex.UUID
 import Remote.WebDAV.DavLocation
+import Types.ProposedAccepted
 
 remote :: RemoteType
-remote = RemoteType
+remote = specialRemoteType $ RemoteType
 	{ typename = "webdav"
 	, enumerate = const (findSpecialRemotes "webdav")
 	, generate = gen
+	, configParser = mkRemoteConfigParser
+		[ optionalStringParser urlField
+			(FieldDesc "(required) url to the WebDAV directory")
+		, optionalStringParser davcredsField HiddenField
+		]
 	, setup = webdavSetup
 	, exportSupported = exportIsSupported
 	, importSupported = importUnsupported
 	}
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+urlField :: RemoteConfigField
+urlField = Accepted "url"
+
+davcredsField :: RemoteConfigField
+davcredsField = Accepted "davcreds"
+
+gen :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
 gen r u c gc rs = new <$> remoteCost gc expensiveRemoteCost
   where
 	new cst = Just $ specialRemote c
@@ -95,9 +108,9 @@
 			, appendonly = False
 			, availability = GloballyAvailable
 			, remotetype = remote
-			, mkUnavailable = gen r u (M.insert "url" "http://!dne!/" c) gc rs
+			, mkUnavailable = gen r u (M.insert urlField (RemoteConfigValue "http://!dne!/") c) gc rs
 			, getInfo = includeCredsInfo c (davCreds u) $
-				[("url", fromMaybe "unknown" (M.lookup "url" c))]
+				[("url", fromMaybe "unknown" $ getRemoteConfigValue urlField c)]
 			, claimUrl = Nothing
 			, checkUrl = Nothing
 			, remoteStateHandle = rs
@@ -107,11 +120,12 @@
 webdavSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
 webdavSetup _ mu mcreds c gc = do
 	u <- maybe (liftIO genUUID) return mu
-	url <- case M.lookup "url" c of
-		Nothing -> giveup "Specify url="
-		Just url -> return url
+	url <- maybe (giveup "Specify url=")
+		(return . fromProposedAccepted)
+		(M.lookup urlField c)
 	(c', encsetup) <- encryptionSetup c gc
-	creds <- maybe (getCreds c' gc u) (return . Just) mcreds
+	pc <- either giveup return . parseRemoteConfig c' =<< configParser remote c'
+	creds <- maybe (getCreds pc gc u) (return . Just) mcreds
 	testDav url creds
 	gitConfigSpecialRemote u c' [("webdav", "true")]
 	c'' <- setRemoteCredPair encsetup c' gc (davCreds u) creds
@@ -255,7 +269,7 @@
 runExport (Just h) a = fromMaybe False <$> liftIO (goDAV h $ safely (a h))
 
 configUrl :: Remote -> Maybe URLString
-configUrl r = fixup <$> M.lookup "url" (config r)
+configUrl r = fixup <$> getRemoteConfigValue urlField (config r)
   where
 	-- box.com DAV url changed
 	fixup = replace "https://www.box.com/dav/" boxComUrl
@@ -335,14 +349,14 @@
 				inLocation d mkCol
 			)
 
-getCreds :: RemoteConfig -> RemoteGitConfig -> UUID -> Annex (Maybe CredPair)
+getCreds :: ParsedRemoteConfig -> RemoteGitConfig -> UUID -> Annex (Maybe CredPair)
 getCreds c gc u = getRemoteCredPairFor "webdav" c gc (davCreds u)
 
 davCreds :: UUID -> CredPairStorage
 davCreds u = CredPairStorage
 	{ credPairFile = fromUUID u
 	, credPairEnvironment = ("WEBDAV_USERNAME", "WEBDAV_PASSWORD")
-	, credPairRemoteField = "davcreds"
+	, credPairRemoteField = davcredsField
 	}
 
 {- Content-Type to use for files uploaded to WebDAV. -}
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -1614,7 +1614,7 @@
 			annexed_present annexedfile
 	{- Ensure the configuration complies with the encryption scheme, and
 	 - that all keys are encrypted properly for the given directory remote. -}
-	testEncryptedRemote scheme ks c keys = case Remote.Helper.Encryptable.extractCipher c of
+	testEncryptedRemote scheme ks c keys = case Remote.Helper.Encryptable.extractCipher pc of
 		Just cip@Crypto.SharedCipher{} | scheme == "shared" && isNothing ks ->
 			checkKeys cip Nothing
 		Just cip@(Crypto.EncryptedCipher encipher v ks')
@@ -1622,6 +1622,8 @@
 				checkKeys cip (Just v) <&&> checkCipher encipher ks'
 		_ -> return False
 	  where
+		pc =either mempty id $
+			Remote.Helper.Encryptable.parseEncryptionConfig c
 		keysMatch (Utility.Gpg.KeyIds ks') =
 			maybe False (\(Utility.Gpg.KeyIds ks2) ->
 					sort (nub ks2) == sort (nub ks')) ks
@@ -1630,7 +1632,7 @@
 		checkScheme Types.Crypto.PubKey = scheme == "pubkey"
 		checkKeys cip mvariant = do
 			dummycfg <- Types.GitConfig.dummyRemoteGitConfig
-			let encparams = (mempty :: Types.Remote.RemoteConfig, dummycfg)
+			let encparams = (mempty :: Types.Remote.ParsedRemoteConfig, dummycfg)
 			cipher <- Crypto.decryptCipher gpgcmd encparams cip
 			files <- filterM doesFileExist $
 				map ("dir" </>) $ concatMap (serializeKeys cipher) keys
diff --git a/Types/Crypto.hs b/Types/Crypto.hs
--- a/Types/Crypto.hs
+++ b/Types/Crypto.hs
@@ -1,11 +1,12 @@
 {- git-annex crypto types
  -
- - Copyright 2011-2015 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 module Types.Crypto (
+	EncryptionMethod(..),
 	Cipher(..),
 	StorableCipher(..),
 	EncryptedCipherVariant(..),
@@ -14,6 +15,7 @@
 	Mac(..),
 	readMac,
 	showMac,
+	macMap,
 	defaultMac,
 	calcMac,
 ) where
@@ -21,6 +23,17 @@
 import Utility.Hash
 import Utility.Gpg (KeyIds(..))
 
+import Data.Typeable
+import qualified Data.Map as M
+
+data EncryptionMethod
+	= NoneEncryption
+	| SharedEncryption
+	| PubKeyEncryption
+	| SharedPubKeyEncryption
+	| HybridEncryption
+	deriving (Typeable, Eq)
+
 -- XXX ideally, this would be a locked memory region
 data Cipher = Cipher String | MacOnlyCipher String
 
@@ -50,9 +63,13 @@
 
 -- Read the MAC algorithm from the remote config.
 readMac :: String -> Maybe Mac
-readMac "HMACSHA1"   = Just HmacSha1
-readMac "HMACSHA224" = Just HmacSha224
-readMac "HMACSHA256" = Just HmacSha256
-readMac "HMACSHA384" = Just HmacSha384
-readMac "HMACSHA512" = Just HmacSha512
-readMac _ = Nothing
+readMac n = M.lookup n macMap
+
+macMap :: M.Map String Mac
+macMap = M.fromList
+	[ ("HMACSHA1", HmacSha1)
+	, ("HMACSHA224", HmacSha224)
+	, ("HMACSHA256", HmacSha256)
+	, ("HMACSHA384", HmacSha384)
+	, ("HMACSHA512", HmacSha512)
+	]
diff --git a/Types/ProposedAccepted.hs b/Types/ProposedAccepted.hs
new file mode 100644
--- /dev/null
+++ b/Types/ProposedAccepted.hs
@@ -0,0 +1,38 @@
+{- proposed and accepted values
+ -
+ - Copyright 2020 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+module Types.ProposedAccepted where
+
+import Test.QuickCheck
+
+-- | A value that may be proposed, or accepted.
+--
+-- When parsing/validating the value, may want to error out on invalid
+-- input. But if a previous version of git-annex accepted an invalid value,
+-- it's too late to error out, and instead the bad value may be ignored.
+data ProposedAccepted t = Proposed t | Accepted t
+	deriving (Show)
+
+fromProposedAccepted :: ProposedAccepted t -> t
+fromProposedAccepted (Proposed t) = t
+fromProposedAccepted (Accepted t) = t
+
+-- | Whether a value is proposed or accepted does not matter when checking
+-- equality.
+instance Eq t => Eq (ProposedAccepted t) where
+	a == b = fromProposedAccepted a == fromProposedAccepted b
+
+-- | Order by the contained value, not by whether it's proposed or
+-- accepted.
+instance Ord t => Ord (ProposedAccepted t) where
+	compare a b = compare (fromProposedAccepted a) (fromProposedAccepted b)
+
+instance Arbitrary t => Arbitrary (ProposedAccepted t) where
+	arbitrary = oneof
+		[ Proposed <$> arbitrary
+		, Accepted <$> arbitrary
+		]
diff --git a/Types/Remote.hs b/Types/Remote.hs
--- a/Types/Remote.hs
+++ b/Types/Remote.hs
@@ -2,7 +2,7 @@
  -
  - Most things should not need this, using Types instead
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -10,8 +10,7 @@
 {-# LANGUAGE RankNTypes #-}
 
 module Types.Remote
-	( RemoteConfigField
-	, RemoteConfig
+	( module Types.RemoteConfig
 	, RemoteTypeA(..)
 	, RemoteA(..)
 	, RemoteStateHandle
@@ -28,7 +27,6 @@
 	)
 	where
 
-import qualified Data.Map as M
 import Data.Ord
 
 import qualified Git
@@ -42,6 +40,7 @@
 import Types.NumCopies
 import Types.Export
 import Types.Import
+import Types.RemoteConfig
 import Config.Cost
 import Utility.Metered
 import Git.Types (RemoteName)
@@ -49,10 +48,6 @@
 import Utility.Url
 import Utility.DataUnits
 
-type RemoteConfigField = String
-
-type RemoteConfig = M.Map RemoteConfigField String
-
 data SetupStage = Init | Enable RemoteConfig
 
 {- There are different types of remotes. -}
@@ -63,13 +58,15 @@
 	-- The Bool is True if automatic initialization of remotes is desired
 	, enumerate :: Bool -> a [Git.Repo]
 	-- generates a remote of this type
-	, generate :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> a (Maybe (RemoteA a))
+	, generate :: Git.Repo -> UUID -> ParsedRemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> a (Maybe (RemoteA a))
+	-- parse configs of remotes of this type
+	, configParser :: RemoteConfig -> a RemoteConfigParser
 	-- initializes or enables a remote
 	, setup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> a (RemoteConfig, UUID)
 	-- check if a remote of this type is able to support export
-	, exportSupported :: RemoteConfig -> RemoteGitConfig -> a Bool
+	, exportSupported :: ParsedRemoteConfig -> RemoteGitConfig -> a Bool
 	-- check if a remote of this type is able to support import
-	, importSupported :: RemoteConfig -> RemoteGitConfig -> a Bool
+	, importSupported :: ParsedRemoteConfig -> RemoteGitConfig -> a Bool
 	}
 
 instance Eq (RemoteTypeA a) where
@@ -124,7 +121,7 @@
 	-- Runs an action to repair the remote's git repository.
 	, repairRepo :: Maybe (a Bool -> a (IO Bool))
 	-- a Remote has a persistent configuration store
-	, config :: RemoteConfig
+	, config :: ParsedRemoteConfig
 	-- Get the git repo for the Remote.
 	, getRepo :: a Git.Repo
 	-- a Remote's configuration from git
diff --git a/Types/RemoteConfig.hs b/Types/RemoteConfig.hs
new file mode 100644
--- /dev/null
+++ b/Types/RemoteConfig.hs
@@ -0,0 +1,69 @@
+{- git-annex remote config types
+ -
+ - Copyright 2020 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE GADTs #-}
+
+module Types.RemoteConfig where
+
+import qualified Data.Map as M
+import qualified Data.Set as S
+import Data.Typeable
+
+import Types.ProposedAccepted
+
+type RemoteConfigField = ProposedAccepted String
+
+{- What the user provides to configure the remote, and what is stored for
+ - later; a bunch of fields and values. -}
+type RemoteConfig = M.Map RemoteConfigField (ProposedAccepted String)
+
+{- Before being used a RemoteConfig has to be parsed. -}
+type ParsedRemoteConfig = M.Map RemoteConfigField RemoteConfigValue
+
+{- Remotes can have configuration values of many types, so use Typeable
+ - to let them all be stored in here. -}
+data RemoteConfigValue where
+	RemoteConfigValue :: Typeable v => v -> RemoteConfigValue
+
+{- Parse a field's value provided by the user into a RemoteConfigValue.
+ -
+ - The RemoteConfig is provided to the parser function for cases
+ - where multiple fields need to be looked at. However, it's important
+ - that, when a parser looks at an additional field in that way, the
+ - parser list contains a dedicated parser for that field as well.
+ - Presence of fields that are not included in this list will cause
+ - a parse failure.
+ -}
+data RemoteConfigFieldParser = RemoteConfigFieldParser
+	{ parserForField :: RemoteConfigField
+	, valueParser :: Maybe (ProposedAccepted String) -> RemoteConfig -> Either String (Maybe RemoteConfigValue)
+	, fieldDesc :: FieldDesc
+	, valueDesc :: Maybe ValueDesc
+	}
+
+data FieldDesc
+	= FieldDesc String
+	| HiddenField
+
+newtype ValueDesc = ValueDesc String
+
+data RemoteConfigParser = RemoteConfigParser
+	{ remoteConfigFieldParsers :: [RemoteConfigFieldParser]
+	, remoteConfigRestPassthrough :: Maybe (RemoteConfigField -> Bool, [(String, FieldDesc)])
+	}
+
+mkRemoteConfigParser :: Monad m => [RemoteConfigFieldParser] -> RemoteConfig -> m RemoteConfigParser
+mkRemoteConfigParser l _ = pure (RemoteConfigParser l Nothing)
+
+addRemoteConfigParser :: [RemoteConfigFieldParser] -> RemoteConfigParser -> RemoteConfigParser
+addRemoteConfigParser l rpc = rpc
+	{ remoteConfigFieldParsers = 
+		remoteConfigFieldParsers rpc ++ filter isnew l
+	}
+  where
+	s = S.fromList (map parserForField (remoteConfigFieldParsers rpc))
+	isnew p = not (S.member (parserForField p) s)
diff --git a/Types/RemoteState.hs b/Types/RemoteState.hs
--- a/Types/RemoteState.hs
+++ b/Types/RemoteState.hs
@@ -9,7 +9,7 @@
 
 import Types.UUID
 
-{- When per-remote state, its UUID is used to identify it.
+{- When there is per-remote state, remotes are identified by UUID.
  -
  - However, sameas remotes mean that two different Remote implementations
  - can be used for the same underlying data store. To avoid them using
diff --git a/Types/StandardGroups.hs b/Types/StandardGroups.hs
--- a/Types/StandardGroups.hs
+++ b/Types/StandardGroups.hs
@@ -11,9 +11,10 @@
 
 import Types.Remote (RemoteConfig)
 import Types.Group
+import Types.ProposedAccepted
+import Annex.SpecialRemote.Config (preferreddirField)
 
 import qualified Data.Map as M
-import Data.Maybe
 
 type PreferredContentExpression = String
 
@@ -71,7 +72,8 @@
 associatedDirectory _ SmallArchiveGroup = Just "archive"
 associatedDirectory _ FullArchiveGroup = Just "archive"
 associatedDirectory (Just c) PublicGroup = Just $
-	fromMaybe "public" $ M.lookup "preferreddir" c
+	maybe "public" fromProposedAccepted $
+		M.lookup preferreddirField c
 associatedDirectory Nothing PublicGroup = Just "public"
 associatedDirectory _ _ = Nothing
 
diff --git a/Utility/Daemon.hs b/Utility/Daemon.hs
--- a/Utility/Daemon.hs
+++ b/Utility/Daemon.hs
@@ -8,7 +8,9 @@
 {-# LANGUAGE CPP #-}
 
 module Utility.Daemon (
+#ifndef mingw32_HOST_OS
 	daemonize,
+#endif
 	foreground,
 	checkDaemon,
 	stopDaemon,
diff --git a/Utility/FileSystemEncoding.hs b/Utility/FileSystemEncoding.hs
--- a/Utility/FileSystemEncoding.hs
+++ b/Utility/FileSystemEncoding.hs
@@ -43,6 +43,7 @@
 import qualified Data.ByteString.UTF8 as S8
 import qualified Data.ByteString.Lazy.UTF8 as L8
 #endif
+import System.FilePath.ByteString (RawFilePath, encodeFilePath, decodeFilePath)
 
 import Utility.Exception
 import Utility.Split
@@ -171,21 +172,11 @@
 encodeBL' = L8.fromString
 #endif
 
-{- Recent versions of the unix package have this alias; defined here
- - for backwards compatibility. -}
-type RawFilePath = S.ByteString
-
-{- Note that the RawFilePath is assumed to never contain NUL,
- - since filename's don't. This should only be used with actual
- - RawFilePaths not arbitrary ByteString that may contain NUL. -}
 fromRawFilePath :: RawFilePath -> FilePath
-fromRawFilePath = decodeBS'
+fromRawFilePath = decodeFilePath
 
-{- Note that the FilePath is assumed to never contain NUL,
- - since filename's don't. This should only be used with actual FilePaths
- - not arbitrary String that may contain NUL. -}
 toRawFilePath :: FilePath -> RawFilePath
-toRawFilePath = encodeBS'
+toRawFilePath = encodeFilePath
 
 {- Converts a [Word8] to a FilePath, encoding using the filesystem encoding.
  -
diff --git a/Utility/RawFilePath.hs b/Utility/RawFilePath.hs
--- a/Utility/RawFilePath.hs
+++ b/Utility/RawFilePath.hs
@@ -31,6 +31,7 @@
 
 #else
 import qualified Data.ByteString as B
+import System.PosixCompat (FileStatus)
 import qualified System.PosixCompat as P
 import qualified System.Directory as D
 import Utility.FileSystemEncoding
diff --git a/Utility/Url.hs b/Utility/Url.hs
--- a/Utility/Url.hs
+++ b/Utility/Url.hs
@@ -1,6 +1,6 @@
 {- Url downloading.
  -
- - Copyright 2011-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2020 Joey Hess <id@joeyh.name>
  -
  - License: BSD-2-clause
  -}
@@ -8,6 +8,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE BangPatterns #-}
 
 module Utility.Url (
 	newManager,
@@ -35,6 +36,10 @@
 	parseURIRelaxed,
 	matchStatusCodeException,
 	matchHttpExceptionContent,
+	BasicAuth(..),
+	GetBasicAuth,
+	noBasicAuth,
+	applyBasicAuth',
 ) where
 
 import Common
@@ -84,6 +89,7 @@
 	, applyRequest :: Request -> Request
 	, httpManager :: Manager
 	, allowedSchemes :: S.Set Scheme
+	, getBasicAuth :: GetBasicAuth
 	}
 
 data UrlDownloader
@@ -101,10 +107,11 @@
 	<*> pure id
 	<*> newManager tlsManagerSettings
 	<*> pure (S.fromList $ map mkScheme ["http", "https", "ftp"])
+	<*> pure noBasicAuth
 
-mkUrlOptions :: Maybe UserAgent -> Headers -> UrlDownloader -> Manager -> S.Set Scheme -> UrlOptions
-mkUrlOptions defuseragent reqheaders urldownloader manager =
-	UrlOptions useragent reqheaders urldownloader applyrequest manager
+mkUrlOptions :: Maybe UserAgent -> Headers -> UrlDownloader -> Manager -> S.Set Scheme -> GetBasicAuth -> UrlOptions
+mkUrlOptions defuseragent reqheaders urldownloader manager getbasicauth =
+	UrlOptions useragent reqheaders urldownloader applyrequest manager getbasicauth
   where
 	applyrequest = \r -> r { requestHeaders = requestHeaders r ++ addedheaders }
 	addedheaders = uaheader ++ otherheaders
@@ -197,14 +204,14 @@
 	Nothing -> return (Right dne)
    where
 	go :: URI -> IO (Either String UrlInfo)
-	go u = case (urlDownloader uo, parseUrlRequest (show u)) of
+	go u = case (urlDownloader uo, parseRequest (show u)) of
 		(DownloadWithConduit (DownloadWithCurlRestricted r), Just req) -> catchJust
 			-- When http redirects to a protocol which 
 			-- conduit does not support, it will throw
 			-- a StatusCodeException with found302
 			-- and a Response with the redir Location.
 			(matchStatusCodeException (== found302))
-			(Right <$> existsconduit req)
+			(Right <$> existsconduit req uo)
 			(followredir r)
 				`catchNonAsync` (const $ return $ Right dne)
 		(DownloadWithConduit (DownloadWithCurlRestricted r), Nothing)
@@ -243,18 +250,28 @@
 	extractfilename = contentDispositionFilename . B8.toString
 		<=< lookup hContentDisposition . responseHeaders
 
-	existsconduit req = do
+	existsconduit req uo' = do
 		let req' = headRequest (applyRequest uo req)
 		debugM "url" (show req')
-		runResourceT $ do
+		join $ runResourceT $ do
 			resp <- http req' (httpManager uo)
 			-- forces processing the response while
 			-- within the runResourceT
 			liftIO $ if responseStatus resp == ok200
-				then found
-					(extractlen resp)
-					(extractfilename resp)
-				else return dne
+				then do
+					let !len = extractlen resp
+					let !fn = extractfilename resp
+					return $ found len fn
+				else if responseStatus resp == unauthorized401
+					then return $ getBasicAuth uo' (show (getUri req)) >>= \case
+						Nothing -> return dne
+						Just (ba, signalsuccess) -> do
+							ui <- existsconduit
+								(applyBasicAuth' ba req)							
+								(uo' { getBasicAuth = noBasicAuth })
+							signalsuccess (urlExists ui)
+							return ui
+					else return $ return dne
 
 	existscurl u curlparams = do
 		output <- catchDefaultIO "" $
@@ -284,6 +301,7 @@
 				sz <- getFileSize' f stat
 				found (Just sz) Nothing
 			Nothing -> return dne
+	
 	followredir r (HttpExceptionRequest _ (StatusCodeException resp _)) = 
 		case headMaybe $ map decodeBS $ getResponseHeader hLocation resp of
 			Just url' -> case parseURIRelaxed url' of
@@ -334,7 +352,7 @@
   where
 	go = case parseURIRelaxed url of
 		Just u -> checkPolicy uo u $
-			case (urlDownloader uo, parseUrlRequest (show u)) of
+			case (urlDownloader uo, parseRequest (show u)) of
 				(DownloadWithConduit (DownloadWithCurlRestricted r), Just req) -> catchJust
 					(matchStatusCodeException (== found302))
 					(downloadConduit meterupdate req file uo >> return (Right ()))
@@ -416,12 +434,18 @@
 downloadConduit meterupdate req file uo =
 	catchMaybeIO (getFileSize file) >>= \case
 		Just sz | sz > 0 -> resumedownload sz
-		_ -> runResourceT $ do
+		_ -> join $ runResourceT $ do
 			liftIO $ debugM "url" (show req')
 			resp <- http req' (httpManager uo)
 			if responseStatus resp == ok200
-				then store zeroBytesProcessed WriteMode resp
-				else respfailure resp
+				then do
+					store zeroBytesProcessed WriteMode resp
+					return (return ())
+				else if responseStatus resp == unauthorized401
+					then return $ getBasicAuth uo (show (getUri req')) >>= \case
+						Nothing -> respfailure resp
+						Just ba -> retryauthed ba
+					else return $ respfailure resp
   where
 	req' = applyRequest uo $ req
 		-- Override http-client's default decompression of gzip
@@ -440,15 +464,23 @@
 		dl
 		(const noop)
 	  where
-		dl = runResourceT $ do
+		dl = join $ runResourceT $ do
 			let req'' = req' { requestHeaders = resumeFromHeader sz : requestHeaders req }
 			liftIO $ debugM "url" (show req'')
 			resp <- http req'' (httpManager uo)
 			if responseStatus resp == partialContent206
-				then store (toBytesProcessed sz) AppendMode resp
+				then do
+					store (toBytesProcessed sz) AppendMode resp
+					return (return ())
 				else if responseStatus resp == ok200
-					then store zeroBytesProcessed WriteMode resp
-					else respfailure resp
+					then do
+						store zeroBytesProcessed WriteMode resp
+						return (return ())
+					else if responseStatus resp == unauthorized401
+						then return $ getBasicAuth uo (show (getUri req'')) >>= \case
+							Nothing -> respfailure resp
+							Just ba -> retryauthed ba
+						else return $ respfailure resp
 	
 	alreadydownloaded sz s h = s == requestedRangeNotSatisfiable416 
 		&& case lookup hContentRange h of
@@ -469,6 +501,18 @@
 	
 	respfailure = giveup . B8.toString . statusMessage . responseStatus
 	
+	retryauthed (ba, signalsuccess) = do
+		r <- tryNonAsync $ downloadConduit
+			meterupdate
+			(applyBasicAuth' ba req)
+			file
+			(uo { getBasicAuth = noBasicAuth })
+		case r of
+			Right () -> signalsuccess True
+			Left e -> do
+				() <- signalsuccess False
+				throwM e
+	
 {- Sinks a Response's body to a file. The file can either be opened in
  - WriteMode or AppendMode. Updates the meter as data is received.
  -
@@ -502,7 +546,7 @@
 	Nothing -> return Nothing
 	Just u -> go u `catchNonAsync` const (return Nothing)
   where
-	go u = case parseUrlRequest (show u) of
+	go u = case parseRequest (show u) of
 		Nothing -> return Nothing
 		Just req -> do
 			let req' = applyRequest uo req
@@ -517,9 +561,6 @@
 parseURIRelaxed s = maybe (parseURIRelaxed' s) Just $
 	parseURI $ escapeURIString isAllowedInURI s
 
-parseUrlRequest :: URLString -> Maybe Request
-parseUrlRequest = parseUrlThrow
-
 {- Some characters like '[' are allowed in eg, the address of
  - an uri, but cannot appear unescaped further along in the uri.
  - This handles that, expensively, by successively escaping each character
@@ -628,3 +669,22 @@
 		, Param "--max-redirs", Param "0"
 		]
 	bracketaddr a = "[" ++ a ++ "]"
+
+data BasicAuth = BasicAuth
+	{ basicAuthUser :: String
+	, basicAuthPassword :: String
+	}
+
+-- Note that this is only used when using conduit, not curl.
+--
+-- The returned IO action is run after trying to use the BasicAuth,
+-- indicating if the password worked.
+type GetBasicAuth = URLString -> IO (Maybe (BasicAuth, Bool -> IO ()))
+
+noBasicAuth :: GetBasicAuth
+noBasicAuth = const $ pure Nothing
+
+applyBasicAuth' :: BasicAuth -> Request -> Request
+applyBasicAuth' ba = applyBasicAuth
+	(encodeBS (basicAuthUser ba))
+	(encodeBS (basicAuthPassword ba))
diff --git a/doc/git-annex-add.mdwn b/doc/git-annex-add.mdwn
--- a/doc/git-annex-add.mdwn
+++ b/doc/git-annex-add.mdwn
@@ -39,6 +39,16 @@
 
   Add gitignored files.
 
+* `--force-large`
+
+  Treat all files as large files, ignoring annex.largefiles configuration,
+  and add to the annex.
+
+* `--force-small`
+
+  Treat all files as small files, ignoring annex.largefiles configuration,
+  and add to git, also ignoring annex.addsmallfiles configuration.
+
 * `--backend`
 
   Specifies which key-value backend to use.
diff --git a/doc/git-annex-enableremote.mdwn b/doc/git-annex-enableremote.mdwn
--- a/doc/git-annex-enableremote.mdwn
+++ b/doc/git-annex-enableremote.mdwn
@@ -27,7 +27,8 @@
   
 Some special remotes may need parameters to be specified every time they are
 enabled. For example, the directory special remote requires a directory=
-parameter every time.
+parameter every time. The command will prompt for any required parameters
+you leave out.
 
 This command can also be used to modify the configuration of an existing
 special remote, by specifying new values for parameters that are
diff --git a/doc/git-annex-initremote.mdwn b/doc/git-annex-initremote.mdwn
--- a/doc/git-annex-initremote.mdwn
+++ b/doc/git-annex-initremote.mdwn
@@ -19,8 +19,11 @@
  
 The remote's configuration is specified by the parameters passed
 to this command. Different types of special remotes need different
-configuration values. The command will prompt for parameters as needed. A
-few parameters that are supported by all special remotes are documented in
+configuration values, so consult the documentation of a special remote for
+details. The command will prompt for any required parameters you leave out;
+you can also pass --whatelse to see additional parameters.
+
+A few parameters that are supported by all special remotes are documented in
 the next section below.
 
 Once a special remote has been initialized once with this command,
@@ -35,6 +38,15 @@
 
 # OPTIONS
 
+* `--whatelse` / `-w`
+
+  Describe additional configuration parameters that you could specify.
+
+  For example, if you know you want a S3 remote, but forget how to
+  configure it:
+
+	git annex initremote mys3 type=S3 --whatelse
+
 * `--fast`
 
   When initializing a remote that uses encryption, a cryptographic key is
@@ -66,7 +78,7 @@
 
 * `encryption`
 
-  All special remotes support encryption. You will need to specify
+  Almost all special remotes support encryption. You will need to specify
   what encryption, if any, to use. 
 
   If you do not want any encryption, use `encryption=none`
@@ -76,6 +88,7 @@
   
   For details about this and other encrpytion settings, see
   <https://git-annex.branchable.com/encryption/>
+  or --whatelse
 
 * `autoenable`
 
diff --git a/doc/git-annex-reinject.mdwn b/doc/git-annex-reinject.mdwn
--- a/doc/git-annex-reinject.mdwn
+++ b/doc/git-annex-reinject.mdwn
@@ -36,8 +36,13 @@
 
   With this option, each specified src file is hashed using the default
   key-value backend (or the one specified with `--backend`), and if git-annex
-  has a record of the file having been in the annex before, the content is
-  reinjected.
+  has a record of the resulting key having been in the annex before, the
+  content is reinjected.
+
+  Note that, when using a key-value backend that includes the filename
+  extension in the key, this will only work if the src files have the same
+  extensions as the files with the same content that was originally added
+  to git-annex.
 
   Note that this will reinject old versions of files that have been
   modified or deleted from the current git branch.
diff --git a/git-annex.cabal b/git-annex.cabal
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,11 +1,11 @@
 Name: git-annex
-Version: 7.20191230
+Version: 7.20200202.7
 Cabal-Version: >= 1.8
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>
 Author: Joey Hess
 Stability: Stable
-Copyright: 2010-2019 Joey Hess
+Copyright: 2010-2020 Joey Hess
 License-File: COPYRIGHT
 Homepage: http://git-annex.branchable.com/
 Build-type: Custom
@@ -296,7 +296,7 @@
 custom-setup
   Setup-Depends: base (>= 4.11.1.0), hslogger, split, unix-compat, process,
     filepath, exceptions, bytestring, directory, IfElse, data-default,
-    filepath-bytestring (>= 1.4.2.1.0),
+    filepath-bytestring (>= 1.4.2.1.1),
     utf8-string, transformers, Cabal
 
 Executable git-annex
@@ -315,13 +315,13 @@
    case-insensitive,
    random,
    dlist,
-   unix-compat,
+   unix-compat (>= 0.5),
    SafeSemaphore,
    async,
    directory (>= 1.2),
    disk-free-space,
    filepath,
-   filepath-bytestring (>= 1.4.2.1.0),
+   filepath-bytestring (>= 1.4.2.1.1),
    IfElse,
    hslogger,
    monad-logger,
@@ -394,7 +394,6 @@
   if (os(windows))
     Build-Depends:
       Win32 (>= 2.6.1.0),
-      unix-compat (>= 0.5),
       setenv,
       process (>= 1.6.2.0),
       silently (>= 1.2.5.1)
@@ -996,8 +995,10 @@
     Types.MetaData
     Types.Mime
     Types.NumCopies
+    Types.ProposedAccepted
     Types.RefSpec
     Types.Remote
+    Types.RemoteConfig
     Types.RemoteState
     Types.RepoVersion
     Types.ScheduledActivity
