Recent changes to this wiki:

Added a comment
diff --git a/doc/forum/Disable_autoswitching_to_adjusted__40__unlocked__41___branch/comment_1_6de4f4546dd797a223186ec536e8a24b._comment b/doc/forum/Disable_autoswitching_to_adjusted__40__unlocked__41___branch/comment_1_6de4f4546dd797a223186ec536e8a24b._comment
new file mode 100644
index 0000000000..7e32b47dca
--- /dev/null
+++ b/doc/forum/Disable_autoswitching_to_adjusted__40__unlocked__41___branch/comment_1_6de4f4546dd797a223186ec536e8a24b._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="yarikoptic"
+ avatar="http://cdn.libravatar.org/avatar/f11e9c84cb18d26a1748c33b48c924b4"
+ subject="comment 1"
+ date="2026-07-21T17:08:46Z"
+ content="""
+I feel like I had asked similar question in the past but failed to find it or an answer ;-)
+
+Indeed it feels like it would be great to  be able to just declare to always do only \"unlocked\" mode operations in a given git-annex repo, and thus potentially keeping it always in the \"unlocked\" mode (even when on system with symlinks support), but without requiring any `adjusted/` \"synchronization\" altogether.
+"""]]

add news item for git-annex 10.20260717
diff --git a/doc/news/version_10.20260420.mdwn b/doc/news/version_10.20260420.mdwn
deleted file mode 100644
index abd090e745..0000000000
--- a/doc/news/version_10.20260420.mdwn
+++ /dev/null
@@ -1,10 +0,0 @@
-git-annex 10.20260420 released with [[!toggle text="these changes"]]
-[[!toggleable text="""  * disableremote: New command.
-  * Fix annexUrl to inherit any password that is set in the remote url.
-  * Add DELEGATE extension to the external special remote protocol.
-  * Avoid dying of an exception when when stdout gets closed by eg head(1),
-    and avoid a crash loop when stderr is closed and git-annex dies of an
-    exception.
-    Fixes reversion introduced in version 10.20230407.
-  * Improve UUID sanitization.
-  * Deal with breaking changes to test concurrency in tasty-1.5.4."""]]
\ No newline at end of file
diff --git a/doc/news/version_10.20260717.mdwn b/doc/news/version_10.20260717.mdwn
new file mode 100644
index 0000000000..37abe8d757
--- /dev/null
+++ b/doc/news/version_10.20260717.mdwn
@@ -0,0 +1,18 @@
+git-annex 10.20260717 released with [[!toggle text="these changes"]]
+[[!toggleable text="""  * External special remote protocol extended to support import.
+    Developers of external special remotes should consider if import makes
+    sense for them and add support.
+  * Support importtree=yes with rsync special remotes.
+  * Added git-annex-remote-internetarchive special remote that imports an
+    Internet Archive item and serves as an example for how to easily write
+    importtree=yes external special remotes.
+    https://git-annex.branchable.com/tips/how\_to\_make\_a\_simple\_importtree\_special\_remote/
+  * Added DOWNLOAD-URL extension to the external special remote protocol.
+  * git-annex is guaranteed to not contain LLM generated code,
+    and will attempt to remain buildable with versions of dependencies
+    that predate the addition of any LLM generated code.
+    See https://git-annex.branchable.com/no\_llm\_code/
+  * git-annex.cabal: Added NoLLMDependencies build flag.
+  * Added stack-NoLLMDependencies.yaml
+  * Avoid redundant hash verification after a hash verification fails.
+  * Fix build with time-1.15."""]]
\ No newline at end of file

fix link
diff --git a/doc/special_remotes/Internet_Archive.mdwn b/doc/special_remotes/Internet_Archive.mdwn
index 9ede028a0b..1a65585e9c 100644
--- a/doc/special_remotes/Internet_Archive.mdwn
+++ b/doc/special_remotes/Internet_Archive.mdwn
@@ -2,6 +2,6 @@
 remote in two different ways:
 
 1. To upload files to it, see
-   [[tips/[The Internet Archive](http://www.archive.org/)]]
+   [[tips/Internet_Archive_via_S3]]
 2. To [[import a tree of files|git-annex-import]] from an existing 
    Internet Archive item, use [[external/git-annex-remote-internetarchive]].

reorg
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index fe8de64306..4e1cc9d942 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -161,16 +161,16 @@ like this:
 
 Similarly, the CHECKPRESENTIMPORT request is git-annex just checking
 if the file still appears to be present in the remote. 
-(It does not need to verify that the has the same content that it did when
-imported, checking the size or even if HTTP HEAD succeeds is enough.
-Although if you can verify the content inexpensively, by all means do so.)
 
     		CHECKPRESENTIMPORT)
     			key="$2"
     			echo CHECKPRESENT-URL "$key" "https://archive.org/download/$item//$importlocation"
     		;;
 
-And if you need to hit the url yourself, you could do something like this:
+If you need to hit the url yourself, you could do something like the
+following. It does not need to verify that the has the same content that it did when
+imported, checking the size or even if HTTP HEAD succeeds is enough.
+Although if you can verify the content inexpensively, by all means do so.
 
             CHECKPRESENTIMPORT)
                 key="$2"

typo
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index e62811063f..fe8de64306 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -126,7 +126,7 @@ well as using its User-Agent etc. Here's how to do that:
                         tmpfile="$@"
                         # now process "$tmpfile" and output IMPORTABLECONTENT
                         # and IMPORTABLECONTENTIDENTIFIER as before
-                        # (Left as an exersise for the reader.)
+                        # (Left as an exercise for the reader.)
                         echo LISTIMPORTABLECONTENTS-SUCCESS
                     ;;
                     DOWNLOAD-URL-FAILURE)

DOWNLOAD-URL
Also LISTIMPORTABLECONTENTS-FAILURE and renamed IMPORTABLECONTENTEND
to LISTIMPORTABLECONTENTS-SUCCESS for consistency.
Note that the temp file that DOWNLOAD-URL writes to is scoped to the
lifetime of the protocol request that git-annex makes.
diff --git a/CHANGELOG b/CHANGELOG
index 24658ba4be..131e564db2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -10,6 +10,7 @@ git-annex (10.20260717) UNRELEASED; urgency=medium
     Developers of external special remotes should consider if import makes
     sense for them and add support.
   * Support importtree=yes with rsync special remotes.
+  * Added DOWNLOAD-URL extension to the external special remote protocol.
   * Avoid redundant hash verification after a hash verification fails.
   * Fix build with time-1.15.
 
diff --git a/Remote/External.hs b/Remote/External.hs
index 4bacaa352b..28ccc382c1 100644
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -34,6 +34,7 @@ import Remote.Helper.ExportImport
 import Remote.Helper.ReadOnly
 import Utility.Metered
 import Utility.Hash
+import Utility.Tmp
 import Types.Transfer
 import Logs.PreferredContent.Raw
 import Logs.RemoteState
@@ -597,12 +598,14 @@ listImportableContentsM external =
 	go c (Just (sz, loc)) (IMPORTABLECONTENTIDENTIFIER cid) =
 		Just $ return $ GetNextMessage $ 
 			go ((loc, (cid, sz)):c) Nothing
-	go c _ IMPORTABLECONTENTEND =
+	go c _ LISTIMPORTABLECONTENTS_SUCCESS =
 		result $ Just $
 			ImportableContentsComplete $ ImportableContents
 				{ importableContents = c
 				, importableHistory = []
 				}
+	go _ _ (LISTIMPORTABLECONTENTS_FAILURE err) =
+		giveup err
 	go _ _ (DELEGATE ps) = Just $ do
 		delegate <- getDelegateRemote external ps
 		Result <$> listImportableContents (importActions delegate)
@@ -721,18 +724,24 @@ handleRequest' st external req mp responsehandler
   where
 	go = do
 		sendMessage st req
-		loop
-	loop = receiveMessage st external responsehandler
-		(\rreq -> Just $ handleRemoteRequest rreq >> loop)
-		(\msg -> Just $ handleExceptionalMessage msg >> loop)
+		cleanupv <- liftIO $ atomically $ newTMVar []
+		loop cleanupv
+			`finally` cleanup cleanupv
+	
+	loop cleanupv = receiveMessage st external responsehandler
+		(\rreq -> Just $ handleRemoteRequest cleanupv rreq >> loop cleanupv)
+		(\msg -> Just $ handleExceptionalMessage msg >> loop cleanupv)
+
+	cleanup cleanupv = liftIO $
+		sequence =<< atomically (takeTMVar cleanupv)
 
-	handleRemoteRequest (PROGRESS bytesprocessed) =
+	handleRemoteRequest _ (PROGRESS bytesprocessed) =
 		maybe noop (\a -> liftIO $ a bytesprocessed) mp
-	handleRemoteRequest (DIRHASH k) = 
+	handleRemoteRequest _ (DIRHASH k) = 
 		send $ VALUE $ fromOsPath $ hashDirMixed def k
-	handleRemoteRequest (DIRHASH_LOWER k) = 
+	handleRemoteRequest _ (DIRHASH_LOWER k) = 
 		send $ VALUE $ fromOsPath $ hashDirLower def k
-	handleRemoteRequest (SETCONFIG setting value) =
+	handleRemoteRequest _ (SETCONFIG setting value) =
 		liftIO $ atomically $ do
 			ParsedRemoteConfig m c <- takeTMVar (externalConfig st)
 			let !m' = M.insert
@@ -747,13 +756,13 @@ handleRequest' st external req mp responsehandler
 			f <- takeTMVar (externalConfigChanges st)
 			let !f' = M.insert (Accepted setting) (Accepted value) . f
 			putTMVar (externalConfigChanges st) f'
-	handleRemoteRequest (GETCONFIG setting) = do
+	handleRemoteRequest _ (GETCONFIG setting) = do
 		value <- maybe "" fromProposedAccepted
 			. (M.lookup (Accepted setting))
 			. unparsedRemoteConfig
 			<$> liftIO (atomically $ readTMVar $ externalConfig st)
 		send $ VALUE value
-	handleRemoteRequest (SETCREDS setting login password) = case (externalUUID external, externalGitConfig external) of
+	handleRemoteRequest _ (SETCREDS setting login password) = case (externalUUID external, externalGitConfig external) of
 		(Just u, Just gc) -> do
 			pc <- liftIO $ atomically $ takeTMVar (externalConfig st)
 			pc' <- setRemoteCredPair' pc encryptionAlreadySetup gc
@@ -769,56 +778,78 @@ handleRequest' st external req mp responsehandler
 				let !f' = M.union configchanges . f
 				putTMVar (externalConfigChanges st) f'
 		_ -> senderror "cannot send SETCREDS here"
-	handleRemoteRequest (GETCREDS setting) = case (externalUUID external, externalGitConfig external) of
+	handleRemoteRequest _ (GETCREDS setting) = case (externalUUID external, externalGitConfig external) of
 		(Just u, Just gc) -> do
 			c <- liftIO $ atomically $ readTMVar $ 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
+	handleRemoteRequest _ GETUUID = case externalUUID external of
 		Just u -> send $ VALUE $ fromUUID u
 		Nothing -> senderror "cannot send GETUUID here"
-	handleRemoteRequest GETGITDIR = 
+	handleRemoteRequest _ GETGITDIR = 
 		send . VALUE . fromOsPath =<< fromRepo Git.localGitDir
-	handleRemoteRequest GETGITREMOTENAME =
+	handleRemoteRequest _ GETGITREMOTENAME =
 		case externalRemoteName external of
 			Just n -> send $ VALUE n
 			Nothing -> senderror "git remote name not known"
-	handleRemoteRequest (SETWANTED expr) = case externalUUID external of
+	handleRemoteRequest _ (SETWANTED expr) = case externalUUID external of
 		Just u -> preferredContentSet u expr
 		Nothing -> senderror "cannot send SETWANTED here"
-	handleRemoteRequest GETWANTED = case externalUUID external of
+	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) =
+	handleRemoteRequest _ (SETSTATE key state) =
 		case externalRemoteStateHandle external of
 			Just h -> setRemoteState h key state
 			Nothing -> senderror "cannot send SETSTATE here"
-	handleRemoteRequest (GETSTATE key) =
+	handleRemoteRequest _ (GETSTATE key) =
 		case externalRemoteStateHandle external of
 			Just h -> do
 				state <- fromMaybe ""
 					<$> getRemoteState h key
 				send $ VALUE state
 			Nothing -> senderror "cannot send GETSTATE here"
-	handleRemoteRequest (SETURLPRESENT key url) =
+	handleRemoteRequest _ (SETURLPRESENT key url) =
 		setUrlPresent key url
-	handleRemoteRequest (SETURLMISSING key url) =
+	handleRemoteRequest _ (SETURLMISSING key url) =
 		setUrlMissing key url
-	handleRemoteRequest (SETURIPRESENT key uri) =
-		withurl (SETURLPRESENT key) uri
-	handleRemoteRequest (SETURIMISSING key uri) =
-		withurl (SETURLMISSING key) uri
-	handleRemoteRequest (GETURLS key prefix) = do
+	handleRemoteRequest cleanupv (SETURIPRESENT key uri) =
+		withurl cleanupv (SETURLPRESENT key) uri
+	handleRemoteRequest cleanupv (SETURIMISSING key uri) =
+		withurl cleanupv (SETURLMISSING key) uri
+	handleRemoteRequest _ (GETURLS key prefix) = do
 		mapM_ (send . VALUE) =<< getUrlsWithPrefix key prefix
 		send (VALUE "") -- end of list
-	handleRemoteRequest (DEBUG msg) = fastDebug "Remote.External" msg
-	handleRemoteRequest (INFO msg) = showInfo (UnquotedString msg)
-	handleRemoteRequest (VERSION _) = senderror "too late to send VERSION"
+	handleRemoteRequest _ (DEBUG msg) = fastDebug "Remote.External" msg
+	handleRemoteRequest _ (INFO msg) = showInfo (UnquotedString msg)
+	handleRemoteRequest cleanupv (DOWNLOAD_URL url) = do
+		case externalGitConfig external of
+			Just gc -> do
+				(tmpf, h) <- liftIO $ do
+					tmpdir <- systemTmpDirectory
+					openTmpFileIn tmpdir (literalOsPath "url")
+				liftIO $ hClose h
+				liftIO $ atomically $ do
+					l <- takeTMVar cleanupv
+					putTMVar cleanupv (removeTmpFile tmpf:l)
+				res <- withUrlOptions (Just gc) $
+					downloadUrl' False UnknownSize 
+						nullMeterUpdate Nothing [url]
+						tmpf
+				case res of
+					Right True -> 
+						send $ DOWNLOAD_URL_SUCCESS (fromOsPath tmpf)
+					Left err -> 
+						send $ DOWNLOAD_URL_FAILURE err
+					Right False -> 
+						send $ DOWNLOAD_URL_FAILURE "download failed"
+			_ -> senderror "cannot send DOWNLOAD-URL here"
+	handleRemoteRequest _ (VERSION _) = senderror "too late to send VERSION"
 
 	handleExceptionalMessage (ERROR err) = giveup $ "external special remote error: " ++ err
 
@@ -832,8 +863,8 @@ handleRequest' st external req mp responsehandler
 		}
 	  where
 		base = replace "/" "_" $ fromUUID u ++ "-" ++ setting
-			
-	withurl mk uri = handleRemoteRequest $ mk $
+	
+	withurl cleanupv mk uri = handleRemoteRequest cleanupv $ mk $
 		setDownloader (show uri) OtherDownloader
 
 sendMessage
diff --git a/Remote/External/Types.hs b/Remote/External/Types.hs
index fe276438ec..8d408a91a6 100644
--- a/Remote/External/Types.hs

(Diff truncated)
todo
diff --git a/doc/todo/fast_import_for_externals_with_importKey.mdwn b/doc/todo/fast_import_for_externals_with_importKey.mdwn
new file mode 100644
index 0000000000..39120b3265
--- /dev/null
+++ b/doc/todo/fast_import_for_externals_with_importKey.mdwn
@@ -0,0 +1,18 @@
+External special remotes using importree=yes are not able to provide
+`importKey`. If they were, when it's possible to query a hash from the API
+the external is using, it would be able to import without downloading the
+file.
+
+`git-annex-remote-internetarchive` is an example of a program that could
+use this.
+
+`importKey` is currently only in ExportImportActions,
+not in ImportActions so would first need to be added to it. Easy.
+
+The external would need to convert whatever ContentIdentifier it chooses to
+use into a key. Eg if it uses a sha1, it would make a SHA1 key. IIRC
+git-annex does not provide an interface to generate a key from a hash
+although it does document the key format and the external could be expected
+to generate it by itself. Or there could be a reply like 
+"IMPORTKEY-SUCCESS SHA1 $hash".
+--[[Joey]]:

version for external importtree
diff --git a/CHANGELOG b/CHANGELOG
index c1404ef6f8..24658ba4be 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,4 @@
-git-annex (10.20260625) UNRELEASED; urgency=medium
+git-annex (10.20260717) UNRELEASED; urgency=medium
 
   * git-annex is guaranteed to not contain LLM generated code,
     and will attempt to remain buildable with versions of dependencies
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index 30faeb1e64..85176101d0 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -17,6 +17,8 @@ Using it looks like this:
 	...
 	> git merge mobydick/master --allow-unrelated-histories
 
+Note that this needs git-annex 10.20260717 or newer.
+
 Here we'll walk through that example program and explain what it does,
 which is an easier start for you building you own than reading the
 [[design/external_special_remote_protocol]] and 
diff --git a/git-annex.cabal b/git-annex.cabal
index 4d81acf611..6d5c99d105 100644
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 10.20260624
+Version: 10.20260717
 Cabal-Version: 1.12
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>

improve
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index 564bbe475d..30faeb1e64 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -95,8 +95,8 @@ about an item, and `jq` is used to extract the information it needs.
     			echo IMPORTABLECONTENTEND
     		;;
 
-Don't worry too much about the details of the above example though. This is
-where you'll code up something entirely different.
+Don't worry too much about the parsing details of the above example though.
+This is where you'll code up something entirely different.
 
 The main thing you need to determine is what content identifier to use
 for a file in your remote. In the example, the Internet Archive has a handy

fix
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index e30f3f549b..564bbe475d 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -153,7 +153,7 @@ And if you need to hit the url yourself, you could do something like this:
     		;;
 
 And that's all! Well, all except for some necessary boilerplate code 
-to handle the rest of the protocol that you should need to modify,
+to handle the rest of the protocol that you should never need to modify,
 feel free to just copy the rest of this.
 
     		IMPORTSUPPORTED)

update
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index a2a4c4a294..e30f3f549b 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -113,14 +113,14 @@ request is git-annex asking that a file be downloaded from the remote.
 (This uses an `$importlocation` while we've not dealt with setting
 yet, see below.)
 
-If anyone can download the url, perhaps with some HTTP basic auth,
-you can just tell git-annex what url to download and let it do
-the work. That's what the example does.
-
     		RETRIEVEIMPORT)
     			echo RETRIEVEIMPORT-URL "https://archive.org/download/$item/$importlocation"
     		;;
 
+If anyone can download the url, perhaps with some HTTP basic auth,
+you can just tell git-annex what url to download and let it do
+the work. That's what the example above does.
+
 If you need to download the url yourself, you'll instead need something
 like this:
     		
@@ -131,8 +131,10 @@ like this:
     		;;
 
 Similarly, the CHECKPRESENTIMPORT request is git-annex just checking
-if the file still appears to be present in the remote. (It does not need to
-verify that the has the same content.)
+if the file still appears to be present in the remote. 
+(It does not need to verify that the has the same content that it did when
+imported, checking the size or even if HTTP HEAD succeeds is enough.
+Although if you can verify the content inexpensively, by all means do so.)
 
     		CHECKPRESENTIMPORT)
     			key="$2"

links
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index 4b31af10ab..a2a4c4a294 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -19,8 +19,8 @@ Using it looks like this:
 
 Here we'll walk through that example program and explain what it does,
 which is an easier start for you building you own than reading the
-[[doc/design/external_special_remote_protocol]] and 
-its [[import_appendix|doc/design/external_special_remote_protocol/export_and_import_appendix]].
+[[design/external_special_remote_protocol]] and 
+its [[import_appendix|design/external_special_remote_protocol/export_and_import_appendix]].
 
 Starting off, it's a portable shell script. But of course you could use
 some other language, probably with better results.

link
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index bcb422d607..4b31af10ab 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -3,7 +3,7 @@ importing a tree of files from it with [[git-annex-import]]? This is
 really simple to build with a short shell script.
 
 As an example, see [[special_remotes/external/git-annex-remote-internetarchive]].
-This treats an [[special_remote/Internet_Archive]] item as an
+This treats an [[special_remotes/Internet_Archive]] item as an
 [[external_special_remote|special_remotes/external]].
 
 Using it looks like this:

link
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
index 24c4dd5a8a..bcb422d607 100644
--- a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -1,5 +1,5 @@
 Would you like to treat some web resource as a git-annex special remote,
-importing a tree of files from it with [[git-annex-importree]]? This is
+importing a tree of files from it with [[git-annex-import]]? This is
 really simple to build with a short shell script.
 
 As an example, see [[special_remotes/external/git-annex-remote-internetarchive]].

close
diff --git a/doc/todo/importree_only_external_remotes.mdwn b/doc/todo/importree_only_external_remotes.mdwn
index 39af61b95f..b86f1a1761 100644
--- a/doc/todo/importree_only_external_remotes.mdwn
+++ b/doc/todo/importree_only_external_remotes.mdwn
@@ -58,3 +58,6 @@ only external remote in about 1 page of shell script:
 			;;
 		esac
 	done
+
+> [[done]], I didn't implement `DOWNLOAD-URL` though, being unsure if it is
+> really an ergonomic benefit worth the complexity. --[[Joey]]

fix
diff --git a/doc/special_remotes/external.mdwn b/doc/special_remotes/external.mdwn
index ebac41afea..372fff0184 100644
--- a/doc/special_remotes/external.mdwn
+++ b/doc/special_remotes/external.mdwn
@@ -23,7 +23,7 @@ It's not hard!
 
 It's *especially* easy to implement special remotes that are read-only
 imports of some web resource. See
-[[tips/how_to_make_a_simple_importree_special_remote]].
+[[tips/how_to_make_a_simple_importtree_special_remote]].
 
 ## libraries
 

tip
diff --git a/doc/special_remotes/external.mdwn b/doc/special_remotes/external.mdwn
index 94b2bad263..ebac41afea 100644
--- a/doc/special_remotes/external.mdwn
+++ b/doc/special_remotes/external.mdwn
@@ -15,12 +15,16 @@ It's not hard!
 * When the user runs `git annex initremote foo type=external externaltype=$bar`,
   it will use your program.
 * See [[design/external_special_remote_protocol]] for what the program
-  needs to do. There's an example at the end of this page.
+  needs to do. There are some examples below.
 * If things don't seem to work, pass `--debug` and you'll see, amoung other
   things, a transcript of git-annex's communication with your program.
 * If you build a new special remote, please add it to the list
   of [[special_remotes]].
 
+It's *especially* easy to implement special remotes that are read-only
+imports of some web resource. See
+[[tips/how_to_make_a_simple_importree_special_remote]].
+
 ## libraries
 
 For Python, there is a [library by Lykos153](https://github.com/Lykos153/AnnexRemote)
@@ -29,11 +33,13 @@ that take care of all the protocol details.
 
 ## examples
 
-Here's an example of using an external special remote to add torrent
-support to git-annex: [[external/git-annex-remote-torrent]]
-
 Here's a simple shell script example, which can easily be adapted
 to run whatever commands you need. Or better, re-written in some better
-language of your choice.
+language of your choice: [[external/example.sh]]
 
-[[!inline pages="special_remotes/external/example.sh" feeds=no]]
+Here's an example Internet Archive item special remote,
+which can be used with [[git-annex-importtree]]:
+[[external/git-annex-remote-internetarchive]]
+
+Here's an example of using an external special remote to add torrent
+support to git-annex: [[external/git-annex-remote-torrent]]
diff --git a/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
new file mode 100644
index 0000000000..24c4dd5a8a
--- /dev/null
+++ b/doc/tips/how_to_make_a_simple_importtree_special_remote.mdwn
@@ -0,0 +1,175 @@
+Would you like to treat some web resource as a git-annex special remote,
+importing a tree of files from it with [[git-annex-importree]]? This is
+really simple to build with a short shell script.
+
+As an example, see [[special_remotes/external/git-annex-remote-internetarchive]].
+This treats an [[special_remote/Internet_Archive]] item as an
+[[external_special_remote|special_remotes/external]].
+
+Using it looks like this:
+
+	> git-annex initremote mobydick type=external encryption=none \
+        externaltype=internetarchive importtree=yes item=moby_dick_librivox
+	> git-annex wanted mobydick include="*.mp3"
+	> git-annex import master --from mobydick
+	list mobydick ok
+	import mobydick mobydick_135_melville_64kb.mp3
+	...
+	> git merge mobydick/master --allow-unrelated-histories
+
+Here we'll walk through that example program and explain what it does,
+which is an easier start for you building you own than reading the
+[[doc/design/external_special_remote_protocol]] and 
+its [[import_appendix|doc/design/external_special_remote_protocol/export_and_import_appendix]].
+
+Starting off, it's a portable shell script. But of course you could use
+some other language, probably with better results.
+
+    #!/bin/sh
+    set -e
+
+This is using version 2 of the protocol, which we start off as follows.
+
+    echo VERSION 2
+
+Probably you'll want some configuration. In this case, it expects
+the user to provide an "item=" parameter, which is the Internet Archive
+item's name. The `GETCONFIG` message can be used to query for such parameters,
+and git-annex will respond with "VALUE whatever". This is a shell
+function since it's needed in a couple of different places below.
+
+    getconfig () {
+    	echo GETCONFIG item
+    	read resp
+    	item=$(echo "$resp" | sed 's/^VALUE \?//')
+    }
+
+Now the meat of the program, which is just a loop that reads lines from
+stdin and dispatches on requests that git-annex sends to it.
+
+    while read line; do
+    	set -- $line
+    	case "$1" in
+
+The INITREMOTE request is sent when `git-annex initremote` is run,
+and it's where you validate any configuration, like the "item" parameter.
+
+    		INITREMOTE)
+    			getconfig
+    			if [ -z "$item" ]; then
+    				echo INITREMOTE-FAILURE "Specify item="
+    			else
+    				echo INITREMOTE-SUCCESS
+    			fi
+    		;;
+
+The PREPARE request is sent when git-annex is starting to use the
+remote, and is just a place to do any one-time startup actions.
+Here it's used to call the `getconfig` function which sets
+"$item", which is used below.
+
+    		PREPARE)
+    			getconfig
+    			echo PREPARE-SUCCESS
+    		;;
+
+Now the main event, the LISTIMPORTABLECONTENTS request is
+sent when git-annex wants to import from your remote.
+It outputs 2 lines for each file in the remote,
+followed by IMPORTABLECONTENTEND.
+
+In the example, the Internet Archive has a JSON API to get information
+about an item, and `jq` is used to extract the information it needs.
+
+    		LISTIMPORTABLECONTENTS)
+    			curl --silent "https://archive.org/metadata/$item/files" | \
+    				jq -r '.result[] | (.name, .size, .sha1)' | \
+    			while read name; do
+    				read size
+    				read sha1
+    				if [ "$sha1" != null ]; then
+    					echo IMPORTABLECONTENT "$size" "$name"
+    					echo IMPORTABLECONTENTIDENTIFIER "$sha1"
+    				fi
+    			done
+    			echo IMPORTABLECONTENTEND
+    		;;
+
+Don't worry too much about the details of the above example though. This is
+where you'll code up something entirely different.
+
+The main thing you need to determine is what content identifier to use
+for a file in your remote. In the example, the Internet Archive has a handy
+sha1 available, which is a perfect content identifier. If you're not so
+lucky, you can use things like the size+mtime of the file as the content
+identifier. It just needs to be unique enough to uniquely identify a
+particular version of a file in the web resource that you're treating as a
+remote. It's ok if it turns out to not be as unique as you think, because
+git-annex of course hashes the files itself, and will notice if a hash
+changed.
+
+Continuing on, and nearing the home stretch, the RETRIEVEIMPORT
+request is git-annex asking that a file be downloaded from the remote.
+(This uses an `$importlocation` while we've not dealt with setting
+yet, see below.)
+
+If anyone can download the url, perhaps with some HTTP basic auth,
+you can just tell git-annex what url to download and let it do
+the work. That's what the example does.
+
+    		RETRIEVEIMPORT)
+    			echo RETRIEVEIMPORT-URL "https://archive.org/download/$item/$importlocation"
+    		;;
+
+If you need to download the url yourself, you'll instead need something
+like this:
+    		
+            RETRIEVEIMPORT)
+                shift 1
+                file="$@"
+                curl -o "$file" "https://archive.org/download/$item/$importlocation"
+    		;;
+
+Similarly, the CHECKPRESENTIMPORT request is git-annex just checking
+if the file still appears to be present in the remote. (It does not need to
+verify that the has the same content.)
+
+    		CHECKPRESENTIMPORT)
+    			key="$2"
+    			echo CHECKPRESENT-URL "$key" "https://archive.org/download/$item//$importlocation"
+    		;;
+
+And if you need to hit the url yourself, you could do something like this:
+
+            CHECKPRESENTIMPORT)
+                key="$2"
+                if curl --output /dev/null --silent --head --fail "https://archive.org/download/$item/$importlocation"; then
+                    echo CHECKPRESENTIMPORT-SUCCESS
+                else
+                    echo CHECKPRESENTIMPORT-FAILURE
+                fi
+    		;;
+
+And that's all! Well, all except for some necessary boilerplate code 

(Diff truncated)
document how to limit
diff --git a/doc/special_remotes/external/git-annex-remote-internetarchive b/doc/special_remotes/external/git-annex-remote-internetarchive
index 241688aaf9..4269d75a3a 100755
--- a/doc/special_remotes/external/git-annex-remote-internetarchive
+++ b/doc/special_remotes/external/git-annex-remote-internetarchive
@@ -10,6 +10,9 @@
 #	git-annex import master --from iaitem
 #	git-annex merge iaitem/master
 #
+# To limit the files that are imported to a particular extension use eg:
+# 	git-annex wanted iaitem include="*.mp3"
+#
 # Copyright 2013-2026 Joey Hess; licenced under the GNU GPL version 3 or higher.
 set -e
 getconfig () {

add git-annex-remote-internetarchive
diff --git a/doc/special_remotes.mdwn b/doc/special_remotes.mdwn
index 01b4567e0d..cb7991f745 100644
--- a/doc/special_remotes.mdwn
+++ b/doc/special_remotes.mdwn
@@ -49,6 +49,7 @@ Here are specific instructions for using git-annex with various services:
 * [[hubiC|tips/using_Hubic]]
 * [[IMAP|forum/special_remote_for_IMAP]]
 * [Internxt Drive](https://pypi.org/project/git-annex-remote-internxt/)
+* [[Internet_Archive]]
 * [[tips/Internet_Archive_via_S3]]
 * [[ipfs]]
 * [[Jottacloud|rclone]]
diff --git a/doc/special_remotes/Internet_Archive.mdwn b/doc/special_remotes/Internet_Archive.mdwn
new file mode 100644
index 0000000000..9ede028a0b
--- /dev/null
+++ b/doc/special_remotes/Internet_Archive.mdwn
@@ -0,0 +1,7 @@
+[The Internet Archive](http://www.archive.org/) can be used as a special 
+remote in two different ways:
+
+1. To upload files to it, see
+   [[tips/[The Internet Archive](http://www.archive.org/)]]
+2. To [[import a tree of files|git-annex-import]] from an existing 
+   Internet Archive item, use [[external/git-annex-remote-internetarchive]].
diff --git a/doc/special_remotes/external/git-annex-remote-internetarchive b/doc/special_remotes/external/git-annex-remote-internetarchive
new file mode 100755
index 0000000000..241688aaf9
--- /dev/null
+++ b/doc/special_remotes/external/git-annex-remote-internetarchive
@@ -0,0 +1,73 @@
+#!/bin/sh
+# This makes an Internet Archive item into a git-annex special remote.
+#
+# Dependencies: curl, jq
+#
+# Install in PATH, and then use as follows:
+# 	git-annex initremote iaitem type=external encryption=none \
+# 		externaltype=internetarchive importtree=yes \
+# 		item=<item name here>
+#	git-annex import master --from iaitem
+#	git-annex merge iaitem/master
+#
+# Copyright 2013-2026 Joey Hess; licenced under the GNU GPL version 3 or higher.
+set -e
+getconfig () {
+	echo GETCONFIG item
+	read resp
+	item=$(echo "$resp" | sed 's/^VALUE \?//')
+}
+
+echo VERSION 2
+while read line; do
+	set -- $line
+	case "$1" in
+		INITREMOTE)
+			getconfig
+			if [ -z "$item" ]; then
+				echo INITREMOTE-FAILURE "Specify item="
+			else
+				echo INITREMOTE-SUCCESS
+			fi
+		;;
+		PREPARE)
+			getconfig
+			echo PREPARE-SUCCESS
+		;;
+		LISTIMPORTABLECONTENTS)
+			curl --silent "https://archive.org/metadata/$item/files" | \
+				jq -r '.result[] | (.name, .size, .sha1)' | \
+			while read name; do
+				read size
+				read sha1
+				if [ "$sha1" != null ]; then
+					echo IMPORTABLECONTENT "$size" "$name"
+					echo IMPORTABLECONTENTIDENTIFIER "$sha1"
+				fi
+			done
+			echo IMPORTABLECONTENTEND
+		;;
+		RETRIEVEIMPORT)
+			echo RETRIEVEIMPORT-URL "https://archive.org/download/$item/$importlocation"
+		;;
+		CHECKPRESENTIMPORT)
+			key="$2"
+			echo CHECKPRESENT-URL "$key" "https://archive.org/download/$item//$importlocation"
+		;;
+		# Below is all the boilerplate needed to make this work as an
+		# importtree-only remote.
+		IMPORTSUPPORTED)
+			echo IMPORTREQUIRED
+		;;
+		IMPORT)
+			shift 1
+			importlocation="$@"
+		;;
+		INITREMOTE)
+			echo INITREMOTE-SUCCESS
+		;;
+		*)
+			echo UNSUPPORTED-REQUEST
+		;;
+	esac
+done
diff --git a/doc/tips/Internet_Archive_via_S3.mdwn b/doc/tips/Internet_Archive_via_S3.mdwn
index b982588e4c..9a21939acd 100644
--- a/doc/tips/Internet_Archive_via_S3.mdwn
+++ b/doc/tips/Internet_Archive_via_S3.mdwn
@@ -1,8 +1,10 @@
 [The Internet Archive](http://www.archive.org/) allows members to upload
-collections using an Amazon S3 
+items using an Amazon S3 
 [compatible API](http://www.archive.org/help/abouts3.txt), and this can
 be used with git-annex's [[special_remotes/S3]] support. 
 
+(If you only want to import files from an Internet Archive item, 
+
 So, you can locally archive things with git-annex, define remotes that
 correspond to "items" at the Internet Archive, and use git-annex to upload
 your files to there. Of course, your use of the Internet Archive must

implement IMPORTREQUIRED
This allows for some very small external special remote programs that
import from somewhere on the web, and don't need any of the rest of the
external special remote protocol to be implemented.
diff --git a/Annex/SpecialRemote/Config.hs b/Annex/SpecialRemote/Config.hs
index e21a9c3e39..9d1ab13c7e 100644
--- a/Annex/SpecialRemote/Config.hs
+++ b/Annex/SpecialRemote/Config.hs
@@ -288,6 +288,10 @@ yesNoParser f mdef fd = genParser yesno f mdef fd
 	yesno "no" = Just False
 	yesno _ = Nothing
 
+yesNoGenerator :: Bool -> String
+yesNoGenerator True = "yes"
+yesNoGenerator False = "no"
+
 trueFalseParser :: RemoteConfigField -> Maybe Bool -> FieldDesc -> RemoteConfigFieldParser
 trueFalseParser f mdef fd = genParser trueFalseParser' f mdef fd
 	(Just (ValueDesc "true or false"))
diff --git a/Remote/External.hs b/Remote/External.hs
index fe9f68cd06..4bacaa352b 100644
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -110,10 +110,10 @@ gen rt externalprogram r u rc gc rs
 		Annex.addCleanupAction (RemoteCleanup u) $ stopExternal external
 		cst <- getCost external r gc c
 		exportsupported <- if exportTree c
-			then checkExportSupported external
+			then isExportSupported' <$> checkExportSupported (Just external)
 			else return False
 		importsupported <- if importTree c
-			then checkImportSupported external
+			then isImportSupported' <$> checkImportSupported (Just external)
 			else return False
 		let exportactions = if exportsupported
 			then ExportActions
@@ -267,37 +267,42 @@ externalSetup externalprogram setgitconfig ss mu remotename _ c gc = do
 
 checkSupportedWith
 	:: Maybe ExternalProgram
-	-> (External -> Annex Bool)
+	-> (Maybe External -> Annex a)
 	-> ParsedRemoteConfig
 	-> RemoteGitConfig
-	-> Annex Bool
+	-> Annex a
 checkSupportedWith Nothing checker c gc = do
 	let externaltype = fromMaybe (giveup "Specify externaltype=") $
 		remoteAnnexExternalType gc <|> getRemoteConfigValue externaltypeField c
 	if externaltype == "readonly"
-		then return False
+		then checker Nothing
 		else checkSupportedWith (Just (ExternalType externaltype)) checker c gc
 checkSupportedWith (Just externalprogram) checker c gc = 
-	checker 
+	checker . Just
 		=<< newExternal externalprogram Nothing c (Just gc) Nothing Nothing
 
-checkExportSupported :: External -> Annex Bool
-checkExportSupported external = go `catchNonAsync` (const (return False))
+checkExportSupported :: Maybe External -> Annex ExportSupported
+checkExportSupported (Just external) = go
+	`catchNonAsync` (const (return (ExportSupported False)))
   where
 	go = handleRequest external EXPORTSUPPORTED Nothing $ \resp -> case resp of
-		EXPORTSUPPORTED_SUCCESS -> result True
-		EXPORTSUPPORTED_FAILURE -> result False
-		UNSUPPORTED_REQUEST -> result False
+		EXPORTSUPPORTED_SUCCESS -> result (ExportSupported True)
+		EXPORTSUPPORTED_FAILURE -> result (ExportSupported False)
+		UNSUPPORTED_REQUEST -> result (ExportSupported False)
 		_ -> Nothing
+checkExportSupported Nothing = return (ExportSupported False)
 
-checkImportSupported :: External -> Annex Bool
-checkImportSupported external = go `catchNonAsync` (const (return False))
+checkImportSupported :: Maybe External -> Annex ImportSupported
+checkImportSupported (Just external) = go
+	`catchNonAsync` (const (return (ImportSupported False)))
   where
 	go = handleRequest external IMPORTSUPPORTED Nothing $ \resp -> case resp of
-		IMPORTSUPPORTED_SUCCESS -> result True
-		IMPORTSUPPORTED_FAILURE -> result False
-		UNSUPPORTED_REQUEST -> result False
+		IMPORTSUPPORTED_SUCCESS -> result (ImportSupported True)
+		IMPORTSUPPORTED_FAILURE -> result (ImportSupported False)
+		IMPORTREQUIRED -> result ImportRequired
+		UNSUPPORTED_REQUEST -> result (ImportSupported False)
 		_ -> Nothing
+checkImportSupported Nothing = return (ImportSupported False)
 
 storeKeyM :: External -> Storer
 storeKeyM external = fileStorer $ \k f p ->
diff --git a/Remote/External/Types.hs b/Remote/External/Types.hs
index 4008769aa3..fe276438ec 100644
--- a/Remote/External/Types.hs
+++ b/Remote/External/Types.hs
@@ -310,6 +310,7 @@ data Response
 	| RENAMEEXPORT_FAILURE Key
 	| IMPORTSUPPORTED_SUCCESS
 	| IMPORTSUPPORTED_FAILURE
+	| IMPORTREQUIRED
 	| IMPORTABLECONTENT Size FilePath
 	| IMPORTABLECONTENTIDENTIFIER ContentIdentifier
 	| IMPORTABLECONTENTEND
@@ -359,6 +360,7 @@ instance Proto.Receivable Response where
 	parseCommand "RENAMEEXPORT-FAILURE" = Proto.parse1 RENAMEEXPORT_FAILURE
 	parseCommand "IMPORTSUPPORTED-SUCCESS" = Proto.parse0 IMPORTSUPPORTED_SUCCESS
 	parseCommand "IMPORTSUPPORTED-FAILURE" = Proto.parse0 IMPORTSUPPORTED_FAILURE
+	parseCommand "IMPORTREQUIRED" = Proto.parse0 IMPORTREQUIRED
 	parseCommand "IMPORTABLECONTENT" = Proto.parse2 IMPORTABLECONTENT
 	parseCommand "IMPORTABLECONTENTIDENTIFIER" = Proto.parse1 IMPORTABLECONTENTIDENTIFIER
 	parseCommand "IMPORTABLECONTENTEND" = Proto.parse0 IMPORTABLECONTENTEND
diff --git a/Remote/Helper/ExportImport.hs b/Remote/Helper/ExportImport.hs
index 0192911da2..23b2ff7a97 100644
--- a/Remote/Helper/ExportImport.hs
+++ b/Remote/Helper/ExportImport.hs
@@ -33,8 +33,8 @@ import Control.Concurrent.STM
 class HasExportUnsupported a where
 	exportUnsupported :: a
 
-instance HasExportUnsupported (ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool) where
-	exportUnsupported = \_ _ -> return False
+instance HasExportUnsupported (ParsedRemoteConfig -> RemoteGitConfig -> Annex ExportSupported) where
+	exportUnsupported = \_ _ -> return (ExportSupported False)
 
 instance HasExportUnsupported (ExportActions Annex) where
 	exportUnsupported = ExportActions
@@ -52,8 +52,8 @@ instance HasExportUnsupported (ExportActions Annex) where
 class HasImportUnsupported a where
 	importUnsupported :: a
 
-instance HasImportUnsupported (ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool) where
-	importUnsupported = \_ _ -> return False
+instance HasImportUnsupported (ParsedRemoteConfig -> RemoteGitConfig -> Annex ImportSupported) where
+	importUnsupported = \_ _ -> return (ImportSupported False)
 
 instance HasImportUnsupported (ImportActions Annex) where
 	importUnsupported = ImportActions
@@ -84,11 +84,11 @@ instance HasExportImportUnsupported (ExportImportActions Annex) where
 	  where
 		nope = giveup "import combined with export not supported"
 
-exportIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
-exportIsSupported = \_ _ -> return True
+exportIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex ExportSupported
+exportIsSupported = \_ _ -> return (ExportSupported True)
 
-importIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
-importIsSupported = \_ _ -> return True
+importIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex ImportSupported
+importIsSupported = \_ _ -> return (ImportSupported True)
 
 exportImportIsSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
 exportImportIsSupported = \_ _ -> return True
@@ -103,7 +103,7 @@ adjustExportImportRemoteType rt = rt { setup = setup' }
 			=<< configParser rt c
 		let checkconfig supported configured configfield cont =
 			let allowed = 
-				( supported rt pc gc 
+				( pure supported
 					<||> exportImportSupported rt pc gc)
 				<&&> pure (not (thirdPartyPopulated rt))
 			in ifM allowed
@@ -128,8 +128,13 @@ adjustExportImportRemoteType rt = rt { setup = setup' }
 						++ fromProposedAccepted exportTreeField
 					)
 			| otherwise = cont
-		checkconfig exportSupported exportTree exportTreeField $
-			checkconfig importSupported importTree importTreeField $
+		exportsupported <- exportSupported rt pc gc
+		importsupported <- importSupported rt pc gc
+		when (isImportRequired importsupported && not (importTree pc)) $
+			giveup $ "This special remote must be configured with " ++ 
+				fromProposedAccepted importTreeField ++ "=" ++ yesNoGenerator True
+		checkconfig (isExportSupported' exportsupported) exportTree exportTreeField $
+			checkconfig (isImportSupported' importsupported) importTree importTreeField $
 				checkexportimport $
 					setup rt st mu remotename cp c gc
 	
@@ -148,7 +153,7 @@ adjustExportImport r rs = do
 		-- Use ExportImportActions even when
 		-- not configured with exporttree=yes,
 		-- when it's supported, since it
-		-- handled content identifiers more
+		-- handles content identifiers more
 		-- strongly than ImportActions does.
 		( importconfigured
 		-- thirdPartyPopulated is handled using 
diff --git a/Types/Remote.hs b/Types/Remote.hs
index 0756ee827b..17e69b9d2d 100644
--- a/Types/Remote.hs
+++ b/Types/Remote.hs
@@ -20,8 +20,13 @@ module Types.Remote
 	, Verification(..)
 	, unVerified
 	, RetrievalSecurityPolicy(..)
+	, ExportSupported(..)
 	, isExportSupported
+	, isExportSupported'
+	, ImportSupported(..)
 	, isImportSupported
+	, isImportSupported'
+	, isImportRequired

(Diff truncated)
allow using -URL responses without negotiating a protocol extension
Versions of git-annex that support the import appendix will always
support those responses. This will allow for simpler and smaller
external programs for import.
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 3c8ba15539..4946dd893c 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -207,9 +207,7 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     Indicates the retrieve failed.
   * `RETRIEVEIMPORT-URL Url`  
     Rather than the special remote downloading content from an url itself,
-    this lets it offload that work to git-annex. This response is a
-    protocol extension; it's only safe to send it to git-annex after
-    it sent an `EXTENSIONS` that included `TRANSFER-RETRIEVE-URL`.
+    this lets it offload that work to git-annex.
   * `DELEGATE type=value [params]`  
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
@@ -227,9 +225,7 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     present in the remote. (Perhaps the remote cannot be contacted.)
   * `CHECKPRESENT-URL Key Url`  
     Rather than the special remote checking an url itself, 
-    this lets it offload that work to git-annex. This response is a protocol
-    extension; it's only safe to send it to git-annex after it sent an
-    `EXTENSIONS` that included `CHECKPRESENT-URL`.
+    this lets it offload that work to git-annex.
   * `DELEGATE type=value [params]`  
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
diff --git a/doc/todo/importree_only_external_remotes.mdwn b/doc/todo/importree_only_external_remotes.mdwn
index 233ab51b30..eb518fb382 100644
--- a/doc/todo/importree_only_external_remotes.mdwn
+++ b/doc/todo/importree_only_external_remotes.mdwn
@@ -33,12 +33,6 @@ only external remote in about 1 page of shell script:
 			PREPARE)
 				echo PREPARE-SUCCESS
 			;;
-			EXTENSIONS)
-				# not strictly necessary because git-annex versions that support
-				# importtree should support these extensions. 
-				# Need to verify if it works w/o this. 
-				echo EXTENSIONS TRANSFER-RETRIEVE-URL CHECKPRESENT-URL
-			;;
 			IMPORTSUPPORTED)
 				echo IMPORTSUPPORTED-REQUIRED
 			;;

todo
diff --git a/doc/todo/importree_only_external_remotes.mdwn b/doc/todo/importree_only_external_remotes.mdwn
new file mode 100644
index 0000000000..233ab51b30
--- /dev/null
+++ b/doc/todo/importree_only_external_remotes.mdwn
@@ -0,0 +1,66 @@
+It could be simpler to write an external special remote that
+imports a tree from some web resource that is tree-like.
+
+Conceptually, all such a special
+remote needs to do is access an url that lists the files in the data
+store, parse it, and then use `RETRIEVEIMPORT-URL` and `CHECKPRESENT-URL`
+to tell git-annex what urls to use.
+
+But currently, the whole external special remote protocol needs to be
+implemented including storing keys. Even if there is no way to write to the
+data store, and even if key-value storage is not appropriate.
+
+If the external special remote had a way to indicate that it only supported
+importtree, it would not even be necessary to configure it with
+importtree=yes. And it could skip implementing the rest of the protocol.
+This could be eg an `IMPORTSUPPORTED-REQUIRED` response.
+
+Also it would be good to have a way for `LISTIMPORTABLECONTENTS`
+to outsource an url download to git-annex. This might take the form
+of a special remote message `DOWNLOAD-URL` that downloads a given url
+to a file and responds with the filename. (The file would be deleted
+once the current request is done.)
+
+Putting all this together, it should be possible to write an importtree
+only external remote in about 1 page of shell script:
+
+	#!/bin/sh
+	set -e
+	echo VERSION 2
+	while read line; do
+		set -- $line
+		case "$1" in
+			PREPARE)
+				echo PREPARE-SUCCESS
+			;;
+			EXTENSIONS)
+				# not strictly necessary because git-annex versions that support
+				# importtree should support these extensions. 
+				# Need to verify if it works w/o this. 
+				echo EXTENSIONS TRANSFER-RETRIEVE-URL CHECKPRESENT-URL
+			;;
+			IMPORTSUPPORTED)
+				echo IMPORTSUPPORTED-REQUIRED
+			;;
+			LISTIMPORTABLECONTENTS)
+				echo DOWNLOAD-URL http://example.com/
+				read file
+				# parse $file here and output IMPORTABLECONTENT and IMPORTABLECONTENTIDENTIFIER lines
+				echo IMPORTABLECONTENTEND
+			;;
+			IMPORT)
+				shift 1
+				importlocation="$@"
+			;;
+			RETRIEVEIMPORT)
+				echo RETRIEVEIMPORT-URL "http://example.com/$importlocation"
+			;;
+			CHECKPRESENTIMPORT)
+				key="$2"
+				echo CHECKPRESENT-URL "$key" "http://example.com/$importlocation"
+			;;
+			*)
+				echo UNSUPPORTED-REQUEST
+			;;
+		esac
+	done

cleanu
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 4659f45eb2..3c8ba15539 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -25,7 +25,7 @@ remote.
 ## export interface
 
 git-annex will use this when the special remote is initialized with
-exporttree=yes and without importtree=yes and indicates that it supports
+exporttree=yes and indicates that it supports
 exports.
 
 These are requests git-annex sends to the special remote program.

wording
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 3ebf7d2cbd..4659f45eb2 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -4,7 +4,7 @@ Some special remotes interface to a key/value datastore using keys that are
 eg hashes, and it won't make sense for them to implement any of this.
 
 When a special remote interfaces with something that looks like a directory
-of files, it can either or both of the interfaces described here.
+of files, it can implement either or both of the interfaces described here.
 
 The export interface can be implemented to allow the
 [[git-annex-export]] command to be used to store file trees on the special

close one todo and open another
diff --git a/doc/todo/importtree_only_remotes.mdwn b/doc/todo/importtree_only_remotes.mdwn
index 5d518feab1..8724320215 100644
--- a/doc/todo/importtree_only_remotes.mdwn
+++ b/doc/todo/importtree_only_remotes.mdwn
@@ -86,3 +86,5 @@ Or by complicating Remote.Helper.ExportImport further..
 
 [[!tag confirmed]]
 [[!tag projects/ICE4]]
+
+> [[done]] --[[Joey]]
diff --git a/doc/todo/importtree_only_remotes/comment_5_3ce0fa1729d716db94bbd2458c319fd2._comment b/doc/todo/importtree_only_remotes/comment_5_3ce0fa1729d716db94bbd2458c319fd2._comment
new file mode 100644
index 0000000000..a65ce54b4a
--- /dev/null
+++ b/doc/todo/importtree_only_remotes/comment_5_3ce0fa1729d716db94bbd2458c319fd2._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2026-07-16T16:19:19Z"
+ content="""
+The external special remote interface is implemented, so I am closing this.
+
+See new [[todo/make_more_special_remotes_support_importtree]]
+"""]]
diff --git a/doc/todo/make_more_special_remotes_support_importtree.mdwn b/doc/todo/make_more_special_remotes_support_importtree.mdwn
new file mode 100644
index 0000000000..df83ef9b92
--- /dev/null
+++ b/doc/todo/make_more_special_remotes_support_importtree.mdwn
@@ -0,0 +1,18 @@
+Now that importtree=yes is supported for special remotes without also
+needing to implement exporttree=yes, it should be possible to make a lot
+more special remotes support tree importing.
+
+These built-in ones could support it:
+
+* webdav
+* S3
+
+    importtree=yes is only supported when versioning is enabled
+    (or when forced when version is not enabled).
+
+    It would be possible to support importtree=yes without exporttree=yes
+    for any S3 bucket with no need of forcing.
+
+Lots of external special remotes could also probably support it.
+That will be left up to their maintainers, but it would make sense to ping
+the maintainers.

working importtree from external special remotes
diff --git a/Remote/External.hs b/Remote/External.hs
index 4f32121637..5266e74b02 100644
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -525,11 +525,6 @@ checkPresentExportImport request srequest delegateaction handlereq external gc k
 			Left $ srequest ++ " not implemented by external special remote"
 		_ -> Nothing
 
-listImportableContentsM
-	:: External
-	-> Annex (Maybe (ImportableContentsChunkable a (ContentIdentifier, ByteSize)))
-listImportableContentsM = undefined
-
 removeExportM :: External -> Key -> ExportLocation -> Annex ()
 removeExportM external k loc = either giveup return =<< go
   where
@@ -585,6 +580,32 @@ renameExportM external k src dest = either giveup return =<< go
 		_ -> Nothing
 	req sk = RENAMEEXPORT sk dest
 
+listImportableContentsM
+	:: External
+	-> Annex (Maybe (ImportableContentsChunkable Annex (ContentIdentifier, ByteSize)))
+listImportableContentsM external =
+	handleRequest external LISTIMPORTABLECONTENTS Nothing
+		(go [] Nothing)
+  where
+	go c _ (IMPORTABLECONTENT sz loc) = 
+		let loc' = mkImportLocation (toOsPath loc)
+		in Just $ return $ GetNextMessage $
+			go c (Just (sz, loc'))
+	go c (Just (sz, loc)) (IMPORTABLECONTENTIDENTIFIER cid) =
+		Just $ return $ GetNextMessage $ 
+			go ((loc, (cid, sz)):c) Nothing
+	go c _ IMPORTABLECONTENTEND =
+		result $ Just $
+			ImportableContentsComplete $ ImportableContents
+				{ importableContents = c
+				, importableHistory = []
+				}
+	go _ _ (DELEGATE ps) = Just $ do
+		delegate <- getDelegateRemote external ps
+		Result <$> listImportableContents (importActions delegate)
+	go _ _ UNSUPPORTED_REQUEST = result Nothing
+	go _ _ _ = Nothing
+
 {- Sends a Request to the external remote, and waits for it to generate
  - a Response. That is fed into the responsehandler, which should return
  - the action to run for it (or Nothing if there's a protocol error).
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 0cc8cbedfa..3ebf7d2cbd 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -255,24 +255,24 @@ git-annex asks for a list of files stored in the special remote:
 
 Which responds:
 
-	CONTENT 100 foo
-	CONTENTIDENTIFIER 100 48511528411921470
-	CONTENT 200 bar
-	CONTENTIDENTIFIER 200 48511528411963410
-	END
+	IMPORTABLECONTENT 100 foo
+	IMPORTABLECONTENTIDENTIFIER 100 48511528411921470
+	IMPORTABLECONTENT 200 bar
+	IMPORTABLECONTENTIDENTIFIER 200 48511528411963410
+	IMPORTABLECONTENTEND
 
 Next git-annex asks for the content of a file to be retrieved.
 
 	IMPORT foo
-	RETRIEVEIMPORTED tmpfile
+	RETRIEVEIMPORT tmpfile
 
 If the file no longer exists, the response would be:
 
-    RETRIEVE-FAILURE file not found
+    RETRIEVEIMPORT-FAILURE file not found
 
 If the file exists, the response would be:
 
-    RETRIEVE-SUCCESS
+    RETRIEVEIMPORT-SUCCESS
 
 What if the file exists but has been modified in the time
 since LISTIMPORTABLECONTENTS? The external special remote
diff --git a/doc/special_remotes/external/example.sh b/doc/special_remotes/external/example.sh
index e3ddee60cd..c0400e178b 100755
--- a/doc/special_remotes/external/example.sh
+++ b/doc/special_remotes/external/example.sh
@@ -5,7 +5,7 @@
 # 
 # Install in PATH as git-annex-remote-directory
 #
-# Copyright 2013 Joey Hess; licenced under the GNU GPL version 3 or higher.
+# Copyright 2013-2026 Joey Hess; licenced under the GNU GPL version 3 or higher.
 
 set -e
 
@@ -90,7 +90,7 @@ dostore () {
 	   && runcmd mv -f "$tmp" "$loc"; then
 		echo TRANSFER-SUCCESS STORE "$key"
 	else
-		echo TRANSFER-FAILURE STORE "$key"
+		echo TRANSFER-FAILURE STORE "$key" "failed to write to file"
 	fi
 	rmdir "$mydirectory/tmp"
 }
@@ -105,10 +105,10 @@ doretrieve () {
 		if runcmd cp "$loc" "$file"; then
 			echo TRANSFER-SUCCESS RETRIEVE "$key"
 		else
-			echo TRANSFER-FAILURE RETRIEVE "$key"
+			echo TRANSFER-FAILURE RETRIEVE "$key" "failed to read file"
 		fi
 	else
-		echo TRANSFER-FAILURE RETRIEVE "$key"
+		echo TRANSFER-FAILURE RETRIEVE "$key" "file does not exist"
 	fi
 }
 
@@ -142,7 +142,7 @@ doremove () {
 		if runcmd rm -f "$loc"; then
 			echo REMOVE-SUCCESS "$key"
 		else
-			echo REMOVE-FAILURE "$key"
+			echo REMOVE-FAILURE "$key" "file removal failed"
 		fi
 	else
 		echo REMOVE-SUCCESS "$key"
@@ -242,6 +242,15 @@ while read line; do
 		# that are required to be supported, so it's fine
 		# to respond to any others with UNSUPPORTED-REQUEST.
 
+		# This is optional, only provided as an example.
+		GETINFO)
+			echo INFOFIELD "repository location"
+			echo INFOVALUE "$mydirectory"
+			echo INFOFIELD "login"
+			echo INFOVALUE "$MYLOGIN"
+			echo INFOEND
+		;;
+
 		# Let's also support exporting...
 		EXPORTSUPPORTED)
 			echo EXPORTSUPPORTED-SUCCESS
@@ -296,14 +305,37 @@ while read line; do
 				echo RENAMEEXPORT-FAILURE "$key"
 			fi
 		;;
-
-		# This is optional, only provided as an example.
-		GETINFO)
-			echo INFOFIELD "repository location"
-			echo INFOVALUE "$mydirectory"
-			echo INFOFIELD "login"
-			echo INFOVALUE "$MYLOGIN"
-			echo INFOEND
+		
+		# Let's also support importing...
+		IMPORTSUPPORTED)
+			echo IMPORTSUPPORTED-SUCCESS
+		;;
+		LISTIMPORTABLECONTENTS)
+			find "$mydirectory" -type f -printf 'IMPORTABLECONTENT %s %P\nIMPORTABLECONTENTIDENTIFIER %s %T@\n'
+			echo IMPORTABLECONTENTEND
+		;;
+		IMPORT)
+			shift 1
+			importlocation="$mydirectory/$@"
+			# No response to this one; this value is used below.
+		;;
+		RETRIEVEIMPORT)
+			shift 1
+			file="$@"
+			# XXX when easy to do, send PROGRESS while transferring the file
+			if [ -e "$importlocation" ]; then
+				if runcmd cp "$importlocation" "$file"; then
+					echo RETRIEVEIMPORT-SUCCESS
+				else
+					echo RETRIEVEIMPORT-FAILURE "failed to read file"
+				fi
+			else
+				echo RETRIEVEIMPORT-FAILURE "does not exist"
+			fi
+		;;
+		CHECKPRESENTIMPORT)
+			key="$2"
+			docheckpresent "$key" "$importlocation"
 		;;
 
 		*)

follow renaming
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 1eeaad7190..0cc8cbedfa 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -181,7 +181,7 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     in bytes. The Name is the name of the file on the remote,
     in the form of a relative path, and may contain path separators,
     whitespace, and other special characters.  
-    Always followed by `CONTENTIDENTIFIER`.
+    Always followed by `IMPORTABLECONTENTIDENTIFIER`.
   * `IMPORTABLECONTENTIDENTIFIER ContentIdentifier`  
     Provide the ContentIdentifier for the previous `CONTENT`.
   * `IMPORTABLECONTENTEND`  

more consistent protocol for import
Note that CHECKPRESENT-URL already worked since code was reused from CHECKPRESENTEXPORT
diff --git a/Remote/External/Types.hs b/Remote/External/Types.hs
index 76059c3312..4008769aa3 100644
--- a/Remote/External/Types.hs
+++ b/Remote/External/Types.hs
@@ -313,6 +313,9 @@ data Response
 	| IMPORTABLECONTENT Size FilePath
 	| IMPORTABLECONTENTIDENTIFIER ContentIdentifier
 	| IMPORTABLECONTENTEND
+	| RETRIEVEIMPORT_SUCCESS
+	| RETRIEVEIMPORT_FAILURE ErrorMsg
+	| RETRIEVEIMPORT_URL URLString
 	| DELEGATE [String]
 	| UNSUPPORTED_REQUEST
 	deriving (Show)
@@ -359,6 +362,9 @@ instance Proto.Receivable Response where
 	parseCommand "IMPORTABLECONTENT" = Proto.parse2 IMPORTABLECONTENT
 	parseCommand "IMPORTABLECONTENTIDENTIFIER" = Proto.parse1 IMPORTABLECONTENTIDENTIFIER
 	parseCommand "IMPORTABLECONTENTEND" = Proto.parse0 IMPORTABLECONTENTEND
+	parseCommand "RETRIEVEIMPORT-SUCCESS" = Proto.parse0 RETRIEVEIMPORT_SUCCESS
+	parseCommand "RETRIEVEIMPORT-FAILURE" = Proto.parse1 RETRIEVEIMPORT_FAILURE
+	parseCommand "RETRIEVEIMPORT-URL" = Proto.parse1 RETRIEVEIMPORT_URL
 	parseCommand "DELEGATE" = Proto.parseList DELEGATE
 	parseCommand "UNSUPPORTED-REQUEST" = Proto.parse0 UNSUPPORTED_REQUEST
 	parseCommand _ = Proto.parseFail
diff --git a/doc/design/external_special_remote_protocol.mdwn b/doc/design/external_special_remote_protocol.mdwn
index 440276de59..56eb250d92 100644
--- a/doc/design/external_special_remote_protocol.mdwn
+++ b/doc/design/external_special_remote_protocol.mdwn
@@ -503,6 +503,8 @@ These protocol extensions are currently supported.
 * `TRANSFER-RETRIEVE-URL`  
   This allows the `TRANSFER-RETRIEVE-URL` response to be used
   in reply to `TRANSFER` and `TRANSFEREXPORT`.
+  It also allows the `RETRIEVEIMPORT-URL` response to be used
+  in reply to `RETRIEVEIMPORT`.
 * `CHECKPRESENT-URL`  
   This allows the `CHECKPRESENT-URL` response to be used
   in reply to `CHECKPRESENT` and `CHECKPRESENTEXPORT`.
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index d759111ab5..1eeaad7190 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -201,10 +201,15 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
   While the transfer is running, the remote can send any number of
   `PROGRESS` messages. Once the transfer is complete, it finishes by
   sending one of these replies:
-  * `TRANSFER-SUCCESS`
+  * `RETRIEVEIMPORT-SUCCESS`
     Indicates that the retrieve was successful.
-  * `TRANSFER-FAILURE ErrorMsg`  
+  * `RETRIEVEIMPORT-FAILURE ErrorMsg`  
     Indicates the retrieve failed.
+  * `RETRIEVEIMPORT-URL Url`  
+    Rather than the special remote downloading content from an url itself,
+    this lets it offload that work to git-annex. This response is a
+    protocol extension; it's only safe to send it to git-annex after
+    it sent an `EXTENSIONS` that included `TRANSFER-RETRIEVE-URL`.
   * `DELEGATE type=value [params]`  
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
@@ -220,6 +225,11 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
   * `CHECKPRESENT-UNKNOWN Key ErrorMsg`  
     Indicates that it is not currently possible to verify if content is
     present in the remote. (Perhaps the remote cannot be contacted.)
+  * `CHECKPRESENT-URL Key Url`  
+    Rather than the special remote checking an url itself, 
+    this lets it offload that work to git-annex. This response is a protocol
+    extension; it's only safe to send it to git-annex after it sent an
+    `EXTENSIONS` that included `CHECKPRESENT-URL`.
   * `DELEGATE type=value [params]`  
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].

clarify
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 770ef5e206..d759111ab5 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -196,8 +196,8 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
   whitespace, and other special characters.  
   No response is made to this message.
 * `RETRIEVEIMPORT File`  
-  Retrieves the content of a file with the previously provided Name
-  from the special remote to the File on local disk.
+  Retrieves the content of a file with the previously provided 
+  `IMPORT` Name from the special remote to the File on local disk.
   While the transfer is running, the remote can send any number of
   `PROGRESS` messages. Once the transfer is complete, it finishes by
   sending one of these replies:
@@ -209,7 +209,7 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
 * `CHECKPRESENTIMPORT Key`  
-  Requests the remote to check if the previously provided Name
+  Requests the remote to check if the previously provided `IMPORT` Name
   is present in it.
   * `CHECKPRESENT-SUCCESS Key`  
     Indicates that a content has been positively verified to be present

branch
diff --git a/doc/todo/importtree_only_remotes/comment_4_7823d9526111213e991343a2285c2a23._comment b/doc/todo/importtree_only_remotes/comment_4_7823d9526111213e991343a2285c2a23._comment
index aa32982938..04d78c9791 100644
--- a/doc/todo/importtree_only_remotes/comment_4_7823d9526111213e991343a2285c2a23._comment
+++ b/doc/todo/importtree_only_remotes/comment_4_7823d9526111213e991343a2285c2a23._comment
@@ -11,5 +11,5 @@ can safely do that. I know it will be much easier and safer to implement
 importtree only external special remotes.
 
 The external special remote interface for this still needs to be
-implemented.
+implemented. Work has begun in the `importonly_external` branch.)
 """]]

use "IMPORT" for consistency with "EXPORT"
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 42cecebb56..770ef5e206 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -189,7 +189,7 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
   * `DELEGATE type=value [params]`  
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
-* `LOCATION Name`  
+* `IMPORT Name`  
   Comes before each of the following requests,
   specifying the name of the file on the remote. It will be in the
   form of a relative path, and may contain path separators,
@@ -253,7 +253,7 @@ Which responds:
 
 Next git-annex asks for the content of a file to be retrieved.
 
-	LOCATION foo
+	IMPORT foo
 	RETRIEVEIMPORTED tmpfile
 
 If the file no longer exists, the response would be:

improved
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index 9f4ac6503e..42cecebb56 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -175,17 +175,20 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
 * `LISTIMPORTABLECONTENTS`  
   Used to get a list of all the files that are stored in the special 
   remote. A block of responses
-  can be made to this, which must always end with `END`.
-  * `CONTENT Size Name`  
+  can be made to this, which must always end with `IMPORTABLECONTENTEND`.
+  * `IMPORTABLECONTENT Size Name`  
     A file stored in the special remote. The Size is its size
     in bytes. The Name is the name of the file on the remote,
     in the form of a relative path, and may contain path separators,
     whitespace, and other special characters.  
     Always followed by `CONTENTIDENTIFIER`.
-  * `CONTENTIDENTIFIER ContentIdentifier`  
+  * `IMPORTABLECONTENTIDENTIFIER ContentIdentifier`  
     Provide the ContentIdentifier for the previous `CONTENT`.
-  * `END`  
+  * `IMPORTABLECONTENTEND`  
     Indicates the end of the block of responses.
+  * `DELEGATE type=value [params]`  
+    Delegate this request to a different type of special remote.
+    See [[delegate_appendix]].
 * `LOCATION Name`  
   Comes before each of the following requests,
   specifying the name of the file on the remote. It will be in the
@@ -198,10 +201,13 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
   While the transfer is running, the remote can send any number of
   `PROGRESS` messages. Once the transfer is complete, it finishes by
   sending one of these replies:
-  * `RETRIEVE-SUCCESS`
+  * `TRANSFER-SUCCESS`
     Indicates that the retrieve was successful.
-  * `RETRIEVE-FAILURE ErrorMsg`  
+  * `TRANSFER-FAILURE ErrorMsg`  
     Indicates the retrieve failed.
+  * `DELEGATE type=value [params]`  
+    Delegate this request to a different type of special remote.
+    See [[delegate_appendix]].
 * `CHECKPRESENTIMPORT Key`  
   Requests the remote to check if the previously provided Name
   is present in it.
@@ -214,6 +220,9 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
   * `CHECKPRESENT-UNKNOWN Key ErrorMsg`  
     Indicates that it is not currently possible to verify if content is
     present in the remote. (Perhaps the remote cannot be contacted.)
+  * `DELEGATE type=value [params]`  
+    Delegate this request to a different type of special remote.
+    See [[delegate_appendix]].
 
 ### import example
 

Support importtree=yes with rsync special remotes
It works fine, but this first importtree-only special remote exposed some
bugs with the implementation:
* initremote accepts importtree=yes exporttree=yes
* git-annex drop --from the remote is not blocked, does not actually remove
content
diff --git a/CHANGELOG b/CHANGELOG
index 70b97f13d3..1a61501996 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,7 @@ git-annex (10.20260625) UNRELEASED; urgency=medium
     See https://git-annex.branchable.com/no_llm_code/
   * git-annex.cabal: Added NoLLMDependencies build flag.
   * Added stack-NoLLMDependencies.yaml
+  * Support importtree=yes with rsync special remotes.
   * Fix build with time-1.15.
 
  -- Joey Hess <id@joeyh.name>  Wed, 01 Jul 2026 14:12:59 -0400
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
index 9e8f1d04a7..824bdfd567 100644
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -1,11 +1,11 @@
 {- A remote that is only accessible by rsync.
  -
- - Copyright 2011-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
-{-# LANGUAGE CPP, OverloadedStrings #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module Remote.Rsync (
 	remote,
@@ -32,6 +32,7 @@ import Annex.Perms
 import Remote.Helper.Special
 import Remote.Helper.ExportImport
 import Remote.Helper.Path
+import Types.Import
 import Types.Export
 import Types.ProposedAccepted
 import Remote.Rsync.RsyncUrl
@@ -52,6 +53,7 @@ import qualified Utility.RawFilePath as R
 
 import qualified Data.Map as M
 import qualified Data.List.NonEmpty as NE
+import Text.Read
 
 remote :: RemoteType
 remote = specialRemoteType $ RemoteType
@@ -64,7 +66,7 @@ remote = specialRemoteType $ RemoteType
 		]
 	, setup = rsyncSetup
 	, exportSupported = exportIsSupported
-	, importSupported = importUnsupported
+	, importSupported = importIsSupported
 	, exportImportSupported = exportImportUnsupported
 	, thirdPartyPopulated = False
 	}
@@ -106,11 +108,15 @@ gen r u rc gc rs = do
 				{ storeExport = storeExportM o
 				, retrieveExport = retrieveExportM o
 				, removeExport = removeExportM o
-				, checkPresentExport = checkPresentExportM o
+				, checkPresentExport = checkPresentImportExportM o
 				, removeExportDirectory = Nothing
 				, renameExport = Just $ renameExportM o
 				}
-			, importActions = importUnsupported
+			, importActions = ImportActions
+				{ listImportableContents = listImportableContentsM o
+				, retrieveImport = retrieveImportM o
+				, checkPresentImport = checkPresentImportExportM o
+				}
 			, exportImportActions = exportImportUnsupported
 			, whereisKey = Nothing
 			, remoteFsck = Nothing
@@ -332,8 +338,8 @@ retrieveExportM o k loc dest p =
   where
 	rsyncurl = mkRsyncUrl o (fromOsPath (fromExportLocation loc))
 
-checkPresentExportM :: RsyncOpts -> Key -> ExportLocation -> Annex Bool
-checkPresentExportM o _k loc = checkPresentGeneric o [rsyncurl]
+checkPresentImportExportM :: RsyncOpts -> Key -> ExportLocation -> Annex Bool
+checkPresentImportExportM o _k loc = checkPresentGeneric o [rsyncurl]
   where
 	rsyncurl = mkRsyncUrl o (fromOsPath (fromExportLocation loc))
 
@@ -348,6 +354,59 @@ removeExportM o _k loc =
 renameExportM :: RsyncOpts -> Key -> ExportLocation -> ExportLocation -> Annex (Maybe ())
 renameExportM _ _ _ _ = return Nothing
 
+listImportableContentsM :: RsyncOpts -> Annex (Maybe (ImportableContentsChunkable Annex (ContentIdentifier, ByteSize)))
+listImportableContentsM o =
+	withRsyncScratchDir $ \tmp -> do
+		opts <- rsyncOptions o
+		let p = rsyncCreateProcess $ opts ++
+			[ Param "--recursive"
+			, Param "--dry-run"
+			, Param $ "--out-format=" ++ formatstring
+			, Param url
+			, Param $ fromOsPath tmp
+			]
+		l <- mapMaybe parse . lines <$> liftIO (readProcess' p)
+		return $ Just $ ImportableContentsComplete $ ImportableContents
+			{ importableContents = l
+			, importableHistory = []
+			}
+  where
+	-- Make the url end in a slash so rsync will avoid prefixing
+	-- filenames it outputs with part of the url.
+	url = fromOsPath $ addTrailingPathSeparator $ toOsPath $ rsyncUrl o
+
+	formatstring = "%l|%M|%L|%n"
+
+	parse s
+		| "/" `isSuffixOf` s = Nothing
+		| otherwise = case splitc '|' s of
+			(ssz:sdate:ssymlink:rest)
+				| not (null ssymlink) -> Nothing
+				| otherwise -> do
+					sz <- readMaybe ssz
+					let loc = mkImportLocation $ toOsPath $
+						rsyncPathUnescape $
+							intercalate "|" rest
+					let cid = ContentIdentifier $ encodeBS $ 
+						ssz ++ "|" ++ sdate
+					Just (loc, (cid, sz))
+			_ -> Nothing
+
+retrieveImportM :: RsyncOpts -> ImportLocation -> [ContentIdentifier] -> OsPath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
+retrieveImportM o loc _ dest gk p =
+	case gk of
+		Right mkkey -> do
+			go Nothing
+			k <- mkkey
+			return (k, UnVerified)
+		Left k -> do
+			v <- verifyKeyContentIncrementally AlwaysVerify k go
+			return (k, v)
+  where
+	go iv = tailVerify iv dest $
+		rsyncRetrieve o [rsyncurl] dest (Just p)
+	rsyncurl = mkRsyncUrl o (fromOsPath (fromImportLocation loc))
+
 {- Rsync params to enable resumes of sending files safely,
  - ensure that files are only moved into place once complete
  -}
diff --git a/Utility/Rsync.hs b/Utility/Rsync.hs
index 1a35aca09c..3bdeeda05c 100644
--- a/Utility/Rsync.hs
+++ b/Utility/Rsync.hs
@@ -1,6 +1,6 @@
 {- various rsync stuff
  -
- - Copyright 2010-2013 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - License: BSD-2-clause
  -}
@@ -13,10 +13,12 @@ module Utility.Rsync (
 	rsyncServerReceive,
 	rsyncUseDestinationPermissions,
 	rsync,
+	rsyncCreateProcess,
 	rsyncUrlIsShell,
 	rsyncUrlIsPath,
 	rsyncProgress,
 	filterRsyncSafeOptions,
+	rsyncPathUnescape,
 ) where
 
 import Common
@@ -69,6 +71,9 @@ rsyncUseDestinationPermissions = Param "--chmod=ugo=rwX"
 rsync :: [CommandParam] -> IO Bool
 rsync = boolSystem "rsync" . rsyncParamsFixup
 
+rsyncCreateProcess :: [CommandParam] -> CreateProcess
+rsyncCreateProcess = proc "rsync" . toCommand . rsyncParamsFixup
+
 {- On Windows, rsync is from msys2, and expects to get msys2 formatted
  - paths to files. (It thinks that C:foo refers to a host named "C").
  - Fix up the Params appropriately. -}
@@ -187,3 +192,17 @@ toMSYS2Path p
 		| otherwise = s
 #endif
 
+{- When listing files with eg --dry-run, rsync escapes some characters
+ - to 3 octal digits. Eg, "\#012" is '\n'
+ -}
+rsyncPathUnescape :: String -> FilePath
+rsyncPathUnescape = go
+  where
+	go [] = []
+	go ('\\':'#':d1:d2:d3:cs)
+		| isOctDigit d1 && isOctDigit d2 && isOctDigit d3 =
+			case (readish [d1], readish [d2], readish [d3]) of
+				(Just n1, Just n2, Just n3) ->
+					chr (n3+8*n2+8*8*n1) : go cs
+				_ -> error "internal"
+	go (c:cs) = c : go cs
diff --git a/doc/special_remotes/rsync.mdwn b/doc/special_remotes/rsync.mdwn
index 8ba16c998f..ad46fab75f 100644
--- a/doc/special_remotes/rsync.mdwn

(Diff truncated)
external special remote import only interface design
diff --git a/doc/design/external_special_remote_protocol.mdwn b/doc/design/external_special_remote_protocol.mdwn
index e85333ae9f..440276de59 100644
--- a/doc/design/external_special_remote_protocol.mdwn
+++ b/doc/design/external_special_remote_protocol.mdwn
@@ -54,9 +54,9 @@ any extensions it wants to use.
 	EXTENSIONS 
 
 Next, git-annex will generally send a message telling the special
-remote to start up. (Or it might send an INITREMOTE or EXPORTSUPPORTED or
-LISTCONFIGS, or perhaps other things in the future, so don't
-hardcode this order.)
+remote to start up. (Or it might send an INITREMOTE or EXPORTSUPPORTED 
+or IMPORTSUPPORTED or LISTCONFIGS, or perhaps other things in the future,
+so don't hardcode this order.)
 
 	PREPARE
 
@@ -118,7 +118,7 @@ The following requests *must* all be supported by the special remote.
 * `PREPARE`  
   Tells the remote that it's time to prepare itself to be used.  
   Only a few requests for details about the remote can come before this
-  (EXTENSIONS, INITREMOTE, EXPORTSUPPORTED and LISTCONFIGS,
+  (EXTENSIONS, INITREMOTE, EXPORTSUPPORTED, IMPORTSUPPORTED and LISTCONFIGS,
   but others may be added later).
   * `PREPARE-SUCCESS`  
     Sent as a response to PREPARE once the special remote is ready for use.
@@ -184,7 +184,7 @@ The following requests *must* all be supported by the special remote.
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
 
-Special remotes can optionally support tree exports and imports,
+Special remotes can optionally support tree exports or imports,
 which makes the [[git-annex-export]] and [[git-annex-import]] commands
 work with them. See the [[export_and_import_appendix]] for
 additional requests that git-annex will make when using special remotes in
diff --git a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
index ce1ec411bf..9f4ac6503e 100644
--- a/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
+++ b/doc/design/external_special_remote_protocol/export_and_import_appendix.mdwn
@@ -4,20 +4,28 @@ Some special remotes interface to a key/value datastore using keys that are
 eg hashes, and it won't make sense for them to implement any of this.
 
 When a special remote interfaces with something that looks like a directory
-of files, the simple export interface can be implemented to allow the
+of files, it can either or both of the interfaces described here.
+
+The export interface can be implemented to allow the
 [[git-annex-export]] command to be used to store file trees on the special
 remote.
 
-If other tools can write files to the special remote too, the import/export
-interface can be implemented. This allows for both [[git-annex-import]] and 
-[[git-annex-export]] to be used with the special remote.
+The import interface can be implemented to allow the
+[[git-annex-import]] command to be used to import file trees from the special
+remote that are written by some other tool or process.
+
+Note that, when a special remote implements both interfaces, the user will
+need to choose at `initremote` time whether to use `importtree=yes` or
+`exporttree=yes`. They won't be allowed to use both, since this interface
+is not designed to safely allow import and export with the same special
+remote.
 
 [[!toc]]
 
-## simple export interface
+## export interface
 
 git-annex will use this when the special remote is initialized with
-exporttree=yes but without importtree=yes and indicates that it supports
+exporttree=yes and without importtree=yes and indicates that it supports
 exports.
 
 These are requests git-annex sends to the special remote program.
@@ -122,13 +130,13 @@ a request, it can reply with `UNSUPPORTED-REQUEST`.
     Delegate this request to a different type of special remote.
     See [[delegate_appendix]].
 
-## import/export interface
+## import interface
 
 (This part is a draft, not implemented yet.)
 
 git-annex will use this interface when the special remote is
-initialized with both exporttree=yes and importtree=yes and indicates
-that it supports both imports and exports.
+initialized with importtree=yes and indicates
+that it supports imports.
 
 ### content identifiers
 
@@ -141,7 +149,8 @@ file. This is called a ContentIdentifier. A good ContentIdentifier needs to:
 * Be as unique as possible, but not necessarily fully unique.
   A hash of the content would be ideal.
   A (size, mtime, inode) tuple is as good a content identifier as git uses in
-  its index.
+  its index. A (size, mtime) tuple will detect most modifications to files,
+  and is usually good enough.
 * Be reasonably short, since it will be stored in the git-annex branch.
 
 It's up to the implementor of a external special remote program what
@@ -156,14 +165,6 @@ program. Once the special remote has finished performing a request,
 it should send one of the listed replies. Or, if it does not
 support a request, it can reply with `UNSUPPORTED-REQUEST`.
 
-* `EXPORTSUPPORTED`  
-  Used to check if a special remote supports exports.
-  Note that this request may be made before or after `PREPARE`.
-  * `EXPORTSUPPORTED-SUCCESS`  
-    Indicates that it makes sense to use this special remote as an export.
-  * `EXPORTSUPPORTED-FAILURE`  
-    Indicates that it does not make sense to use this special remote as an
-    export.
 * `IMPORTSUPPORTED`  
   Used to check if a special remote supports imports.
   Note that this request may be made before or after `PREPARE`.
@@ -171,13 +172,6 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     Indicates that it makes sense to import from this special remote.
   * `IMPORTSUPPORTED-FAILURE`  
     Indicates that it does not make sense to import from this special remote.
-* `IMPORTKEYSUPPORTED`
-  Used to check if a special remote supports `IMPORTKEY`.
-  Note that this request may be made before or after `PREPARE`.
-  * `IMPORTKEYSUPPORTED-SUCCESS`  
-    Indicates that `IMPORTKEY` can be used.
-  * `IMPORTKEYSUPPORTED-FAILURE`  
-    Indicates that `IMPORTKEY` cannot be used.
 * `LISTIMPORTABLECONTENTS`  
   Used to get a list of all the files that are stored in the special 
   remote. A block of responses
@@ -190,56 +184,17 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     Always followed by `CONTENTIDENTIFIER`.
   * `CONTENTIDENTIFIER ContentIdentifier`  
     Provide the ContentIdentifier for the previous `CONTENT`.
-  * `HISTORY`  
-    When a special remote stores historical versions of files,
-    this can be used to list those versions. It opens a new
-    block of responses. This can be repeated any number of times
-    (indicating a branching history), and histories can also
-    be nested multiple levels deep.  
-    This should only be a response when the remote supports using
-    "TRANSFER RECEIVE Key" to retrieve historical versions of files,
-    and when "GETCONFIG versioning" yields "VALUE TRUE".
   * `END`  
-    Indicates the end of a block of responses.
+    Indicates the end of the block of responses.
 * `LOCATION Name`  
-  Comes before each of the following requests (except for
-  REMOVEEXPORTDIRECTORYWHENEMPTY), 
+  Comes before each of the following requests,
   specifying the name of the file on the remote. It will be in the
   form of a relative path, and may contain path separators,
   whitespace, and other special characters.  
   No response is made to this message.
-* `EXPECTED ContentIdentifier`  
-  Comes before each of the following requests (except
-  for REMOVEEXPORTDIRECTORYWHENEMPTY), specifying the
-  ContentIdentifier that is expected to be present on the remote.
-* `NOTHINGEXPECTED`  
-  If no ContentIdentifier is expected to be present, this is sent
-  rather than `EXPECTED`.
-* `IMPORTKEY File`
-  This only needs to be implemented if IMPORTKEYSUPPORTED indicates
-  it is supported. And if a remote did not support it before, adding
-  it will make importing the same content as before generate a likely
-  different tree, which can lead to merge conflicts. So be careful
-  implementing this.  
-  Generates a key by querying the remote for eg, a checksum.
-  (See [[internals/key_format]] for details of how to format a key.)
-  Any kind of key can be generated, depending on what the remote
-  can support.  
-  The user expects this to be reasonably fast and not use a lot of disk
-  space. It should not download the whole content of the file from the
-  remote.  
-  Must take care to generate a key for the same content as the 
-  ContentIdentifier specified by `EXPECTED`, or otherwise fail.  
-  Replies:
-  * `IMPORTKEY-SUCCESS Key`
-  * `IMPORTKEY-SKIP`  
-    This causes nothing to be imported for this file.
-  * `IMPORTKEY-FAILURE ErrorMsg`
-* `RETRIEVEEXPORTEXPECTED File`  
-  Retrieves the content of a file from the special remote
-  to the File on local disk. Must take care to only retrieve
-  content that has the ContentIdentifier specified by 
-  `EXPECTED`.  
+* `RETRIEVEIMPORT File`  
+  Retrieves the content of a file with the previously provided Name
+  from the special remote to the File on local disk.
   While the transfer is running, the remote can send any number of
   `PROGRESS` messages. Once the transfer is complete, it finishes by
   sending one of these replies:
@@ -247,63 +202,24 @@ support a request, it can reply with `UNSUPPORTED-REQUEST`.
     Indicates that the retrieve was successful.
   * `RETRIEVE-FAILURE ErrorMsg`  
     Indicates the retrieve failed.
-* `STOREEXPORTEXPECTED Key File`  
-  Stores the content of File on the local disk to the previously
-  provided Name on the remote. If the Name already exists on the remote,
-  must take care to only overwrite it when it has the

(Diff truncated)
comment
diff --git a/doc/todo/import_tree_from_rsync_special_remote/comment_9_ae02364bc36d738a6b049e84560246fd._comment b/doc/todo/import_tree_from_rsync_special_remote/comment_9_ae02364bc36d738a6b049e84560246fd._comment
new file mode 100644
index 0000000000..5243126696
--- /dev/null
+++ b/doc/todo/import_tree_from_rsync_special_remote/comment_9_ae02364bc36d738a6b049e84560246fd._comment
@@ -0,0 +1,30 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 9"""
+ date="2026-07-13T17:52:28Z"
+ content="""
+This is now possible to implement, since git-annex has an interface for
+importtree-only special remotes.
+
+To list, it could avoid checksums and use:
+
+	rsync -a --dry-run --out-format='%l|%M|%L|%n' $repo empty-directory
+
+The '%l' field is the file size and combined with the '%M' field's mtime,
+would constitute the ContentIdentifier. Rsync uses a hardcoded mtime
+format like "2026/07/13-12:49:50" which git-annex could parse and convert
+to seconds-from-epoch to save a little bit of space.
+
+The '%L' field will be non-empty when it's a symlink, and those would be skipped.
+
+The '%n' field is the filename, which may be prefixed with a common top-level directory or module name.
+Note that rsync escapes some characters, eg `\#012` is `\n`.
+
+---
+
+Size+mtime as a ContentIdentifier is good enough, it only would prevent
+git-annex from noticing and updating the import when:
+
+1. Two files with the same size and mtime got swapped with one-another.
+2. A file is modified in place without updating the mtime or changing its size.
+"""]]

support for import-only remotes
This will allow making remotes support importtree=yes that cannot safely
support exporttree=yes at the same time.
Note that adjustExportImport takes care of populating importActions
for a remote that has exportImportActions populated but does not
populate importActions itself. This allows for eg Remote.Directory
to not bother with separately populating importActions, which would be a
lot of redundant code. Annex.Import relies on that when it uses eg
listImportableContents.
diff --git a/Annex/Export.hs b/Annex/Export.hs
index 6d2a6226e9..738ed07170 100644
--- a/Annex/Export.hs
+++ b/Annex/Export.hs
@@ -31,10 +31,12 @@ exportKey sha = mk <$> catKey sha
 warnExportImportConflict :: Remote -> Annex ()
 warnExportImportConflict r = do
 	isexport <- Remote.isExportSupported r
+	isimport <- Remote.isImportSupported r
 	isexportimport <- Remote.isExportImportSupported r
-	let (ops, resolvcmd) = case (isexport, isexportimport) of
-		(False, True) -> ("exported to and/or imported from", "git-annex import")
-		(True, False) -> ("exported to", "git-annex export")
+	let (ops, resolvcmd) = case (isexport, isimport, isexportimport) of
+		(False, _, True) -> ("exported to and/or imported from", "git-annex import")
+		(True, _, False) -> ("exported to", "git-annex export")
+		(False, True, False) -> ("imported from", "git-annex import")
 		_ -> ("exported to and/or imported from", "git-annex export")
 	toplevelWarning True $ UnquotedString $ unwords
 		[ "Conflict detected. Different trees have been"
diff --git a/Annex/Import.hs b/Annex/Import.hs
index a48f0bdacb..ea5393225c 100644
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -807,7 +807,7 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 			return (Right job)
 	
 	thirdpartypopulatedimport db (loc, (cid, sz)) = 
-		case Remote.importKey ia of
+		case Remote.importKey (Remote.exportImportActions remote) of
 			Nothing -> return Nothing
 			Just importkey ->
 				tryNonAsync (importkey loc cid sz nullMeterUpdate) >>= \case
@@ -827,7 +827,7 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 		-- than downloading and retrieving a key, to avoid
 		-- generating trees with different keys for the same content.
 		let act = if importcontent
-			then case Remote.importKey ia of
+			then case Remote.importKey (Remote.exportImportActions remote) of
 				Nothing -> dodownload
 				Just _ -> if Utility.Matcher.introspect matchNeedsFileContent (fst matcher)
 					then dodownload
@@ -836,7 +836,7 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 		act cidmap (loc, (cid, sz)) f matcher
 
 	doimport cidmap (loc, (cid, sz)) f matcher =
-		case Remote.importKey ia of
+		case Remote.importKey (Remote.exportImportActions remote) of
 			Nothing -> error "internal" -- checked earlier
 			Just importkey -> do
 				when (Utility.Matcher.introspect matchNeedsFileContent (fst matcher)) $
@@ -883,8 +883,9 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 		getcontent k = do
 			let af = AssociatedFile (Just f)
 			let downloader p' tmpfile = do
-				_ <- Remote.retrieveExportWithContentIdentifier
-					ia loc [cid] tmpfile
+				_ <- Remote.retrieveImport
+					(Remote.importActions remote)
+					loc [cid] tmpfile
 					(Left k)
 					(combineMeterUpdate p' p)
 				ok <- moveAnnex k tmpfile
@@ -901,8 +902,9 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 	-- need to retrieve this file.
 	doimportsmall cidmap loc cid sz p = do
 		let downloader tmpfile = do
-			(k, _) <- Remote.retrieveExportWithContentIdentifier
-				ia loc [cid] tmpfile
+			(k, _) <- Remote.retrieveImport
+				(Remote.importActions remote)
+				loc [cid] tmpfile
 				(Right (mkkey tmpfile))
 				p
 			case keyGitSha k of
@@ -924,8 +926,9 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 	dodownload cidmap (loc, (cid, sz)) f matcher = do
 		let af = AssociatedFile (Just f)
 		let downloader tmpfile p = do
-			(k, _) <- Remote.retrieveExportWithContentIdentifier
-				ia loc [cid] tmpfile
+			(k, _) <- Remote.retrieveImport
+				(Remote.importActions remote)
+				loc [cid] tmpfile
 				(Right (mkkey tmpfile))
 				p
 			case keyGitSha k of
@@ -971,8 +974,6 @@ importKeys remote importtreeconfig importcontent thirdpartypopulated importablec
 						}
 					fst <$> genKey ks nullMeterUpdate backend
 				else gitShaKey <$> hashFile tmpfile
-	
-	ia = Remote.exportImportActions remote
 				
 	bwlimit = remoteAnnexBwLimitDownload (Remote.gitconfig remote)
 			<|> remoteAnnexBwLimit (Remote.gitconfig remote)
@@ -1096,7 +1097,7 @@ pruneImportMatcher = Utility.Matcher.pruneMatcher matchNeedsKey
  -}
 getImportableContents :: Remote -> ImportTreeConfig -> CheckGitIgnore -> FileMatcher Annex -> Annex (Maybe (ImportableContentsChunkable Annex (ContentIdentifier, ByteSize)))
 getImportableContents r importtreeconfig ci matcher = do
-	Remote.listImportableOrExportedContents (Remote.exportImportActions r) >>= \case
+	Remote.listImportableContents (Remote.importActions r) >>= \case
 		Just (ImportableContentsComplete ic) -> do
 			dbhandle <- opendbhandle
 			Just . ImportableContentsComplete
diff --git a/Remote/Adb.hs b/Remote/Adb.hs
index c7aa32d9ac..a784ae1232 100644
--- a/Remote/Adb.hs
+++ b/Remote/Adb.hs
@@ -54,6 +54,7 @@ remote = specialRemoteType $ RemoteType
 		]
 	, setup = adbSetup
 	, exportSupported = exportIsSupported
+	, importSupported = importUnsupported
 	, exportImportSupported = exportImportIsSupported
 	, thirdPartyPopulated = False
 	}
@@ -97,6 +98,7 @@ gen r u rc gc rs = do
 			, removeExportDirectory = Just $ removeExportDirectoryM serial adir
 			, renameExport = Just $ renameExportM serial adir
 			}
+		, importActions = importUnsupported
 		, exportImportActions = ExportImportActions
 			{ listImportableOrExportedContents = listImportableOrExportedContentsM serial adir c
 			, importKey = Nothing
diff --git a/Remote/BitTorrent.hs b/Remote/BitTorrent.hs
index d500f5c00b..0923fc9205 100644
--- a/Remote/BitTorrent.hs
+++ b/Remote/BitTorrent.hs
@@ -47,6 +47,7 @@ remote = RemoteType
 	, configParser = mkRemoteConfigParser []
 	, setup = giveup "not supported"
 	, exportSupported = exportUnsupported
+	, importSupported = importUnsupported
 	, exportImportSupported = exportImportUnsupported
 	, thirdPartyPopulated = False
 	}
@@ -78,6 +79,7 @@ gen r _ rc gc rs = do
 		, checkPresent = checkKey
 		, checkPresentCheap = False
 		, exportActions = exportUnsupported
+		, importActions = importUnsupported
 		, exportImportActions = exportImportUnsupported
 		, whereisKey = Nothing
 		, remoteFsck = Nothing
diff --git a/Remote/Borg.hs b/Remote/Borg.hs
index 506d4366c7..c242c310b6 100644
--- a/Remote/Borg.hs
+++ b/Remote/Borg.hs
@@ -59,6 +59,7 @@ remote = RemoteType
 		]
 	, setup = borgSetup
 	, exportSupported = exportUnsupported
+	, importSupported = importUnsupported
 	, exportImportSupported = exportImportIsSupported
 	, thirdPartyPopulated = True
 	}
@@ -94,6 +95,7 @@ gen r u rc gc rs = do
 		, checkPresent = checkPresentDummy
 		, checkPresentCheap = borgLocal borgrepo
 		, exportActions = exportUnsupported
+		, importActions = importUnsupported
 		, exportImportActions = ExportImportActions
 			{ listImportableOrExportedContents = listImportableOrExportedContentsM u borgrepo c
 			, importKey = Just ThirdPartyPopulated.importKey
diff --git a/Remote/Bup.hs b/Remote/Bup.hs
index d221ab3c4a..5ff440ff45 100644
--- a/Remote/Bup.hs
+++ b/Remote/Bup.hs
@@ -54,6 +54,7 @@ remote = specialRemoteType $ RemoteType
 		]
 	, setup = bupSetup
 	, exportSupported = exportUnsupported
+	, importSupported = importUnsupported
 	, exportImportSupported = exportImportUnsupported
 	, thirdPartyPopulated = False
 	}
@@ -87,6 +88,7 @@ gen r u rc gc rs = do
 		, checkPresent = checkPresentDummy
 		, checkPresentCheap = bupLocal buprepo
 		, exportActions = exportUnsupported
+		, importActions = importUnsupported
 		, exportImportActions = exportImportUnsupported
 		, whereisKey = Nothing
 		, remoteFsck = Nothing
diff --git a/Remote/Compute.hs b/Remote/Compute.hs
index a21fd2f7af..e443f94643 100644
--- a/Remote/Compute.hs
+++ b/Remote/Compute.hs
@@ -85,6 +85,7 @@ remote = RemoteType
 	, configParser = computeConfigParser
 	, setup = setupInstance
 	, exportSupported = exportUnsupported
+	, importSupported = importUnsupported
 	, exportImportSupported = exportImportUnsupported
 	, thirdPartyPopulated = False
 	}
@@ -119,6 +120,7 @@ gen r u rc gc rs = case getComputeProgram' rc of
 		, checkPresent = checkKey rs
 		, checkPresentCheap = False

(Diff truncated)
despam
diff --git a/doc/forum/only_keep_current_version_of_files_in_repo/comment_3_afd16a2b05d3e6fda6078fc4178ac3c2._comment b/doc/forum/only_keep_current_version_of_files_in_repo/comment_3_afd16a2b05d3e6fda6078fc4178ac3c2._comment
deleted file mode 100644
index 46aa03452c..0000000000
--- a/doc/forum/only_keep_current_version_of_files_in_repo/comment_3_afd16a2b05d3e6fda6078fc4178ac3c2._comment
+++ /dev/null
@@ -1,8 +0,0 @@
-[[!comment format=mdwn
- username="nanalyly"
- avatar="http://cdn.libravatar.org/avatar/e9307232a8b6942ccf35b73d26fa2ee3"
- subject="comment 3"
- date="2026-07-13T01:43:14Z"
- content="""
-I also like the idea of having the laptop avoid storing annexed files locally, since it keeps the system lightweight while still allowing access when needed. Although <a href=\"https://sloperidergame.com\">Slope Rider</a> is unrelated to file management, I appreciate how both require thoughtful planning and organization to achieve the best results. I would also make sure the backup process is tested regularly so I can be confident that every important file can be restored if necessary.
-"""]]

update
diff --git a/doc/todo/importtree_only_remotes.mdwn b/doc/todo/importtree_only_remotes.mdwn
index 1728406876..5d518feab1 100644
--- a/doc/todo/importtree_only_remotes.mdwn
+++ b/doc/todo/importtree_only_remotes.mdwn
@@ -51,24 +51,31 @@ What is needed in such an interface?
 	-- strongly verified if this is used to download a particular Key
 	-- that was at one point stored on the remote, since the content
 	-- of the remote could change at any time.
-	-- (The MeterUpdate does not need to be used if 
-        -- sequentially to the file.)
+	-- (The MeterUpdate does not need to be used if it writes
+    -- sequentially to the file.)
 	-- Throws exception on failure.
 	retrieveImport :: ImportLocation -> FilePath -> MeterUpdate -> Annex ()
 	-- Checks if anything is present on the remote at the specified
 	-- ImportLocation. It may check the size or other characteristics
-        -- of the Key, but does not need to guarantee that the content on
-        -- the remote is the same as the Key's content.
-        -- Throws an exception if the remote cannot be accessed.
+    -- of the Key, but does not need to guarantee that the content on
+    -- the remote is the same as the Key's content.
+    -- Throws an exception if the remote cannot be accessed.
 	checkPresentImport :: Key -> ImportLocation -> Annex Bool
 
 listImportableContents is unchanged, and checkPresentImport above
-is identical to checkPresentExport. retrieveImport is very similar
-to retrieveExport, except that the content retrieved is not guaranteed
-to be the same as the content of any key. Actually, it may be an identical
-interface; the only thing I can find that uses retrieveExport forces
-verification of the content retrived since it could have been changed by
-another writer.
+is identical to checkPresentExport.
+
+retrieveImport is very similar to retrieveExport, except that the content
+retrieved is not a particular Key, and that it does not provide any
+Verification.
+
+An alternative to the above `retrieveImport` would be to use 
+`retrieveExport` for getting known keys from the remote, and use
+`retrieveExportWithContentIdentifier` with its current interface for
+importing new ContentIdentifiers from the remote. It would not need
+to ensure that the file really has the ContentIdentifier, but this leaves
+open the possibility of importtree only remotes that do at least some 
+verification of the ContentIdentifier.
 
 The similarity with interface that we already have suggests that
 perhaps this does not need changes to Types.Remote to implement.
diff --git a/doc/todo/importtree_only_remotes/comment_2_ca4f23bcb20bafec2f4d72049b4ed339._comment b/doc/todo/importtree_only_remotes/comment_2_ca4f23bcb20bafec2f4d72049b4ed339._comment
new file mode 100644
index 0000000000..04d0a71a12
--- /dev/null
+++ b/doc/todo/importtree_only_remotes/comment_2_ca4f23bcb20bafec2f4d72049b4ed339._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2026-07-13T13:08:53Z"
+ content="""
+I think it would actually be better to modify Types.Remote and add a new
+interface specifically for this, than to try to jam it into an existing
+interface it was not designed for.
+"""]]

Added a comment
diff --git a/doc/forum/only_keep_current_version_of_files_in_repo/comment_3_afd16a2b05d3e6fda6078fc4178ac3c2._comment b/doc/forum/only_keep_current_version_of_files_in_repo/comment_3_afd16a2b05d3e6fda6078fc4178ac3c2._comment
new file mode 100644
index 0000000000..46aa03452c
--- /dev/null
+++ b/doc/forum/only_keep_current_version_of_files_in_repo/comment_3_afd16a2b05d3e6fda6078fc4178ac3c2._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="nanalyly"
+ avatar="http://cdn.libravatar.org/avatar/e9307232a8b6942ccf35b73d26fa2ee3"
+ subject="comment 3"
+ date="2026-07-13T01:43:14Z"
+ content="""
+I also like the idea of having the laptop avoid storing annexed files locally, since it keeps the system lightweight while still allowing access when needed. Although <a href=\"https://sloperidergame.com\">Slope Rider</a> is unrelated to file management, I appreciate how both require thoughtful planning and organization to achieve the best results. I would also make sure the backup process is tested regularly so I can be confident that every important file can be restored if necessary.
+"""]]

2026
diff --git a/doc/thanks.mdwn b/doc/thanks.mdwn
index 0ddf71eaaf..8c7a4e2343 100644
--- a/doc/thanks.mdwn
+++ b/doc/thanks.mdwn
@@ -19,7 +19,7 @@ from [[!inline raw=yes pages="thanks/contributor-list"]] and
 many others who have [[contributed|contribute]] documentation fixes,
 tips, user support, etc. John Lawrence made the logo.
 
-## financial support, 2024-2025
+## financial support, 2024-2026
 
 <img alt="OpenNeuro logo" src="https://raw.githubusercontent.com/OpenNeuroOrg/openneuro/1c1e0d3b2a2032729727702eb65b1b563eadce1d/packages/openneuro-components/src/assets/on-dark.svg" width=100>&nbsp;&nbsp;
 <img alt="Stanford wordmark" src="https://poldracklab.org/images/stanford-line2-8.png" width=200>&nbsp;&nbsp;

fzj logo hotlink broke
Could not find a good svg one on their website,
although I did find a lot of png ones here
https://www.fz-juelich.de/static/media/Logo.2ceb35fc.svg/@@search?SearchableText=Julich%20Forschungszentrum%20logo&active=relevance&allow_local=false&group_select=4&hl=true&local=false&metadata_fields=effective&metadata_fields=UID&metadata_fields=start&path_prefix=%2Fstatic%2Fmedia%2FLogo.2ceb35fc.svg%2F&use_site_search_settings=1
Hotlinking to wikimedia commons feels a bit naughty, but the traffic on
this page is probably very low.
diff --git a/doc/thanks.mdwn b/doc/thanks.mdwn
index 89e20d22f7..0ddf71eaaf 100644
--- a/doc/thanks.mdwn
+++ b/doc/thanks.mdwn
@@ -23,7 +23,7 @@ tips, user support, etc. John Lawrence made the logo.
 
 <img alt="OpenNeuro logo" src="https://raw.githubusercontent.com/OpenNeuroOrg/openneuro/1c1e0d3b2a2032729727702eb65b1b563eadce1d/packages/openneuro-components/src/assets/on-dark.svg" width=100>&nbsp;&nbsp;
 <img alt="Stanford wordmark" src="https://poldracklab.org/images/stanford-line2-8.png" width=200>&nbsp;&nbsp;
-<img alt="Jülich Forschungszentrum logo" src="https://www.fz-juelich.de/static/media/Logo.2ceb35fc.svg" width=200>&nbsp;&nbsp;
+<img alt="Jülich Forschungszentrum logo" src="https://upload.wikimedia.org/wikipedia/commons/4/40/Logo_des_Forschungszentrums_J%C3%BClich_seit_2018.svg" width=200>&nbsp;&nbsp;
 <img alt="DANDI logo" src="https://dandiarchive.org/assets/logo-DbYqjGgV.svg" width=150>&nbsp;&nbsp;
 <img alt="ReproNim logo" src="https://repronim.org/images/square-512T2.png" width=70>&nbsp;&nbsp;
 

git-annex-browser
diff --git a/doc/related_software.mdwn b/doc/related_software.mdwn
index 44580bbf30..353941f2d9 100644
--- a/doc/related_software.mdwn
+++ b/doc/related_software.mdwn
@@ -71,4 +71,7 @@ designed to interoperate with it.
 
 * [forgejo-aneksajo](https://codeberg.org/matrss/forgejo-aneksajo) is a soft-fork of Forgejo (a git forge) that integrates support for git-annex.
 
+* [git-annex-browser](https://github.com/janttsu/git-annex-browser) is a
+  terminal UI for exploring git-annex repositories.
+
 See also [[not]] for software that is *not* related to git-annex, but similar.

fixed
diff --git a/doc/bugs/auth.mdwn b/doc/bugs/auth.mdwn
index c45cea90ba..f49438eb73 100644
--- a/doc/bugs/auth.mdwn
+++ b/doc/bugs/auth.mdwn
@@ -69,3 +69,5 @@ My expectation would be that a git-annex push would be able to trigger the oauth
 `git-annex version: 10.20260316-gf01ba218ffb36e8607516d9895dfaeaeaf101a05` on Debian forky/sid
 
 [[!tag projects/INM7]]
+
+> [[fixed]] in forgejo-aneksajo --[[Joey]]
diff --git a/doc/bugs/auth/comment_11_bfc8f3d5be99e86efd79dfe3b55e395e._comment b/doc/bugs/auth/comment_11_bfc8f3d5be99e86efd79dfe3b55e395e._comment
new file mode 100644
index 0000000000..1a91e48a45
--- /dev/null
+++ b/doc/bugs/auth/comment_11_bfc8f3d5be99e86efd79dfe3b55e395e._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 11"""
+ date="2026-07-02T15:58:22Z"
+ content="""
+<https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/121>
+"""]]

generalize
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 0b579a15fa..349ed1f501 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -9,8 +9,9 @@ important aspect of its design.
 So, git-annex does not contain code generated by LLMs and guarantees it
 will never do so.
 
-However, libraries that git-annex depends on do not generally have such
-guarantees. Although it would be very much appreciated if they did.
+However, libraries and other things that git-annex depends on do not
+generally have such guarantees. Although it would be very much appreciated
+if they did.
 
 git-annex currently supports being built with versions of
 dependencies that pre-date any introduction of LLM generated code.

git
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 66d9ce7a62..0b579a15fa 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -99,3 +99,11 @@ and 10,000+ lines of changes.
 Cabal is needed to build git-annex, but is not linked into it.
 There is a risk that a new version of Cabal could need changes to
 git-annex.cabal that prevent an old version building it.
+
+### git
+
+Since 2.53
+
+[First LLM generated code](https://github.com/git/git/commit/d7971544fe17378f44f49983010dbfc1834f7bef)
+
+git-annex supports git back to 2.22.

verified fixed
diff --git a/doc/bugs/git_annex_checkpresentkey_removes_git_credentials.mdwn b/doc/bugs/git_annex_checkpresentkey_removes_git_credentials.mdwn
index d9e2ac1002..332de67489 100644
--- a/doc/bugs/git_annex_checkpresentkey_removes_git_credentials.mdwn
+++ b/doc/bugs/git_annex_checkpresentkey_removes_git_credentials.mdwn
@@ -106,3 +106,4 @@ $
 ### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
 
 
+> [[fixed|done]] at some point after 10.20251029. --[[Joey]]
diff --git a/doc/bugs/git_annex_checkpresentkey_removes_git_credentials/comment_5_2eb904c8c743cffd704e4d785d5c453a._comment b/doc/bugs/git_annex_checkpresentkey_removes_git_credentials/comment_5_2eb904c8c743cffd704e4d785d5c453a._comment
new file mode 100644
index 0000000000..cb54da6541
--- /dev/null
+++ b/doc/bugs/git_annex_checkpresentkey_removes_git_credentials/comment_5_2eb904c8c743cffd704e4d785d5c453a._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2026-07-02T15:36:07Z"
+ content="""
+I am not able to reproduce this with current git-annex. 
+
+But, I was able to reproduce it with git-annex 10.20251029.
+
+I have not bisected to find the fix, but will close this.
+"""]]

link
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 1cc0fdd426..66d9ce7a62 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -1,6 +1,7 @@
-LLM generated code in free software is a potential landmine. The copyright
-of such code is an open question, and any current answer to that question
-risks changing at some point in the future.
+[LLM](https://en.wikipedia.org/wiki/Large_language_model) generated code in
+free software is a potential landmine. The copyright of such code is an
+open question, and any current answer to that question risks changing at
+some point in the future.
 
 This is a particular problem for git-annex since [[future proofing]] is an
 important aspect of its design.

typo
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 2d3d53caea..1cc0fdd426 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -13,7 +13,7 @@ guarantees. Although it would be very much appreciated if they did.
 
 git-annex currently supports being built with versions of
 dependencies that pre-date any introduction of LLM generated code.
-To do do, turn on the `NoLLMDependencies` build flag.
+To do so, turn on the `NoLLMDependencies` build flag.
 When building with stack, use `stack-NoLLMDependencies.yaml`.
 (It is not currently built that way by default, but such builds are welcome.)
 

Added stack-NoLLMDependencies.yaml
diff --git a/CHANGELOG b/CHANGELOG
index c3e93c2e0a..70b97f13d3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ git-annex (10.20260625) UNRELEASED; urgency=medium
     that predate the addition of any LLM generated code.
     See https://git-annex.branchable.com/no_llm_code/
   * git-annex.cabal: Added NoLLMDependencies build flag.
+  * Added stack-NoLLMDependencies.yaml
   * Fix build with time-1.15.
 
  -- Joey Hess <id@joeyh.name>  Wed, 01 Jul 2026 14:12:59 -0400
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 31bf0fff9a..2d3d53caea 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -14,6 +14,7 @@ guarantees. Although it would be very much appreciated if they did.
 git-annex currently supports being built with versions of
 dependencies that pre-date any introduction of LLM generated code.
 To do do, turn on the `NoLLMDependencies` build flag.
+When building with stack, use `stack-NoLLMDependencies.yaml`.
 (It is not currently built that way by default, but such builds are welcome.)
 
 Unfortunately, it's not possible to guarantee that will continue to work
diff --git a/git-annex.cabal b/git-annex.cabal
index 1a010768e1..6a920de2e0 100644
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -36,6 +36,7 @@ Description:
 Extra-Source-Files:
   stack.yaml
   stack-botan.yaml
+  stack-NoLLMDependencies.yaml
   README
   CHANGELOG
   NEWS
diff --git a/stack-NoLLMDependencies.yaml b/stack-NoLLMDependencies.yaml
new file mode 100644
index 0000000000..3fcb296182
--- /dev/null
+++ b/stack-NoLLMDependencies.yaml
@@ -0,0 +1,30 @@
+flags:
+  git-annex:
+    NoLLMDependencies: true
+    production: true
+    parallelbuild: true
+    assistant: true
+    torrentparser: true
+    magicmime: false
+    dbus: false
+    debuglocks: false
+    benchmark: true
+    ospath: true
+    botan: false
+    blake3: true
+    xxh3: true
+  file-io:
+    os-string: true
+  xxhash-ffi:
+    pkg-config: false
+packages:
+- '.'
+resolver: lts-24.26
+extra-deps:
+- aws-0.25.2
+- file-io-0.2.0
+- blake3-0.3
+- xxhash-ffi-0.3.1
+- ram-0.20.1
+- persistent-2.14.6.3
+- persistent-sqlite-2.13.3.0

update
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index b3b0717e41..31bf0fff9a 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -18,11 +18,13 @@ To do do, turn on the `NoLLMDependencies` build flag.
 
 Unfortunately, it's not possible to guarantee that will continue to work
 in new versions of git-annex. That's the goal, but it may become
-untenable. It's already the case that building git-annex with the older
-versions of dependencies leaves at least one security hole in a dependency
-open. See below for details about this and other possible future problems
+untenable. See below for details about possible future problems
 with specific dependencies.
 
+Note that if a security hole is only fixed by a newer version of a
+dependency, the `NoLLMDependencies` build flag will still build with the
+older, insecure version.
+
 Additional work needs to be done on an ongoing basis to review
 git-annex's dependencies to detect the addition of LLM generated code.
 

correction
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index e0ed75f24e..b3b0717e41 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -56,9 +56,7 @@ with apparently broken (how?) changes in 0.21.0 being reverted in
 0.21.1.
 
 Rather than use ram, git-annex continues to use the unmaintained
-[memory](//hackage.haskell.org/package/memory). 
-Since git-annex's use of this package is fairly incidental, this
-dependency could be removed if it becomes a problem.
+[memory](//hackage.haskell.org/package/memory) that ram was forked from.
 
 But ram is an dependency of other dependencies, and these in particular
 depend on 0.21.0 or newer:

simplify NoLLMDependencies by depending on older version of ram
That prevents cabal from picking dependencies that need a newer version
of ram which contains LLM generated code.
Note that git-annex still depends on memory. And so all imports from
memory need to be PackageImports qualified now.
It would be easier if git-annex could use the old version of ram.
The problem is that it needs the ByteArrayAccess instance for Digest.
When crypton switched to ram, it depended immediately on the newer
version. So there is no available instance for git-annex to use.
diff --git a/Annex/Balanced.hs b/Annex/Balanced.hs
index 394cb56e00..9005440ad6 100644
--- a/Annex/Balanced.hs
+++ b/Annex/Balanced.hs
@@ -5,6 +5,8 @@
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE PackageImports #-}
+
 module Annex.Balanced where
 
 import Key
@@ -15,7 +17,7 @@ import Data.Maybe
 import qualified Data.List as L
 import Data.Bits (shiftL)
 import qualified Data.Set as S
-import qualified Data.ByteArray as BA
+import qualified "memory" Data.ByteArray as BA
 
 -- The Int is how many UUIDs to pick.
 type BalancedPicker = S.Set UUID -> Key -> Int -> [UUID]
diff --git a/Annex/DirHashes.hs b/Annex/DirHashes.hs
index 21756dea37..e2fb08d5e8 100644
--- a/Annex/DirHashes.hs
+++ b/Annex/DirHashes.hs
@@ -5,6 +5,8 @@
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE PackageImports #-}
+
 module Annex.DirHashes (
 	Hasher,
 	HashLevels(..),
@@ -20,7 +22,7 @@ module Annex.DirHashes (
 import Data.Default
 import Data.Bits
 import qualified Data.List.NonEmpty as NE
-import qualified Data.ByteArray as BA
+import qualified "memory" Data.ByteArray as BA
 import qualified Data.ByteString as S
 
 import Common
diff --git a/Annex/Import.hs b/Annex/Import.hs
index 67b845ddd5..170bf4aeeb 100644
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -6,6 +6,7 @@
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
 {-# LANGUAGE CPP #-}
 
 module Annex.Import (
@@ -71,7 +72,7 @@ import Backend.Utilities
 import Control.Concurrent.STM
 import qualified Data.Map.Strict as M
 import qualified Data.Set as S
-import qualified Data.ByteArray.Encoding as BA
+import qualified "memory" Data.ByteArray.Encoding as BA
 #ifdef mingw32_HOST_OS
 import qualified System.FilePath.Posix as Posix
 #endif
diff --git a/Utility/AuthToken.hs b/Utility/AuthToken.hs
index 4f94c984a1..46a7c4a7b6 100644
--- a/Utility/AuthToken.hs
+++ b/Utility/AuthToken.hs
@@ -25,7 +25,7 @@ import Utility.Exception
 
 import Data.Maybe
 import Data.Char
-import qualified Data.ByteArray as BA
+import qualified "memory" Data.ByteArray as BA
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as TE
 import qualified Data.ByteString.Lazy as L
diff --git a/Utility/Hash/Crypton.hs b/Utility/Hash/Crypton.hs
index bb49a7a2bc..4d290d0150 100644
--- a/Utility/Hash/Crypton.hs
+++ b/Utility/Hash/Crypton.hs
@@ -69,7 +69,7 @@ module Utility.Hash.Crypton (
 import qualified Data.ByteString as S
 import qualified Data.ByteString.Lazy as L
 import Data.IORef
-import qualified Data.ByteArray as BA
+import qualified "memory" Data.ByteArray as BA
 import "crypton" Crypto.Hash
 
 import Utility.Hash.Types
diff --git a/Utility/Hash/Types.hs b/Utility/Hash/Types.hs
index acc3ef97e7..9705876013 100644
--- a/Utility/Hash/Types.hs
+++ b/Utility/Hash/Types.hs
@@ -6,12 +6,13 @@
  -}
 
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE PackageImports #-}
 
 module Utility.Hash.Types where
 
 import qualified Data.ByteString as S
-import Data.ByteArray
-import qualified Data.ByteArray.Encoding as BAE
+import "memory" Data.ByteArray
+import qualified "memory" Data.ByteArray.Encoding as BAE
 import Data.String
 import Control.DeepSeq
 import GHC.Generics
diff --git a/Utility/IPAddress.hs b/Utility/IPAddress.hs
index e19cecac1d..cde446b642 100644
--- a/Utility/IPAddress.hs
+++ b/Utility/IPAddress.hs
@@ -6,6 +6,7 @@
  -}
 
 {-# LANGUAGE BinaryLiterals #-}
+{-# LANGUAGE PackageImports #-}
 
 -- Note that some extensions are necessary for reasons outlined in
 -- my July 2021 blog post. -- JEH
@@ -21,7 +22,7 @@ import Utility.Exception
 
 import Network.Socket
 import Data.Word
-import Data.Memory.Endian
+import "memory" Data.Memory.Endian
 import Data.List
 import Text.Printf
 
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 258b1d0337..e0ed75f24e 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -55,23 +55,22 @@ Note particularly [large LLM generated code churn](https://github.com/jappeace/r
 with apparently broken (how?) changes in 0.21.0 being reverted in
 0.21.1.
 
-Rather than depend on ram, git-annex continues to depend on the unmaintained
+Rather than use ram, git-annex continues to use the unmaintained
 [memory](//hackage.haskell.org/package/memory). 
 Since git-annex's use of this package is fairly incidental, this
 dependency could be removed if it becomes a problem.
 
-But ram is an dependency of other dependencies:
+But ram is an dependency of other dependencies, and these in particular
+depend on 0.21.0 or newer:
 
 * crypton since 1.1.0
-* crypton-x509-validation since 1.9.0. Note that 1.9.1 fixes a security
-  hole, [CVE-2026-9648](https://www.cve.org/CVERecord?id=CVE-2026-9648).
-  This security hole is not expected to affect many git-annex users.
-* aws since 0.25.3 (needed to get the CVE-2026-9648 fix)
-* tls since 2.3.0
-* blake3 since 0.3.1 (optional git-annex dependency)
+* tls since 2.3.1
 
 [Reverse dependencies of ram](https://packdeps.haskellers.com/reverse/ram)
 
+The `NoLLMDependencies` build flag depends on an older version of ram
+in order to prevent such dependencies using the newer version.
+
 ### persistent
 
 [persistent](https://hackage.haskell.org/package/persistent) since 2.15.0.0
diff --git a/git-annex.cabal b/git-annex.cabal
index d627e0f14c..1a010768e1 100644
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -292,8 +292,11 @@ Executable git-annex
    servant-client-core,
    warp (>= 3.2.8),
    warp-tls (>= 3.2.2),
+   crypton,
    crypton-connection (>= 0.4.3),
-   crypton-x509-store
+   crypton-x509-store,
+   tls,
+   aws (>= 0.24.1)
   CC-Options: -Wall
   GHC-Options: -Wall -fno-warn-tabs  -Wincomplete-uni-patterns
   Default-Language: Haskell2010
@@ -305,17 +308,11 @@ Executable git-annex
   if flag(NoLLMDependencies)
     Build-Depends:
      base (>= 4.18.2.1 && < 4.23),
-     crypton (< 1.1.0),
-     crypton-x509-validation (< 1.9.0),
-     aws (>= 0.24.1) && (< 2.25.3),
-     tls (< 2.3.0),
+     ram (< 0.21.0),
      persistent (>= 2.13.3) && (< 2.15.0.0)
   else
     Build-Depends:
      base (>= 4.18.2.1 && < 5),
-     crypton,
-     aws (>= 0.24.1),
-     tls,

(Diff truncated)
update
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index a11f18e350..258b1d0337 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -20,11 +20,11 @@ Unfortunately, it's not possible to guarantee that will continue to work
 in new versions of git-annex. That's the goal, but it may become
 untenable. It's already the case that building git-annex with the older
 versions of dependencies leaves at least one security hole in a dependency
-open.
+open. See below for details about this and other possible future problems
+with specific dependencies.
 
-This means that additional work needs to be done to review git-annex's
-dependencies on an ongoing basis to detect the addition of LLM generated
-code.
+Additional work needs to be done on an ongoing basis to review
+git-annex's dependencies to detect the addition of LLM generated code.
 
 Help with finding these is welcome. Please edit this page and/or file
 bug reports on git-annex if it cannot be built without LLM generated code.
@@ -63,8 +63,9 @@ dependency could be removed if it becomes a problem.
 But ram is an dependency of other dependencies:
 
 * crypton since 1.1.0
-* crypton-x509-validation since 1.9.0. Note that 1.9.1 fixes 
-  [CVE-2026-9648](https://www.cve.org/CVERecord?id=CVE-2026-9648).
+* crypton-x509-validation since 1.9.0. Note that 1.9.1 fixes a security
+  hole, [CVE-2026-9648](https://www.cve.org/CVERecord?id=CVE-2026-9648).
+  This security hole is not expected to affect many git-annex users.
 * aws since 0.25.3 (needed to get the CVE-2026-9648 fix)
 * tls since 2.3.0
 * blake3 since 0.3.1 (optional git-annex dependency)

git-annex.cabal: Added NoLLMDependencies build flag
diff --git a/CHANGELOG b/CHANGELOG
index 2f70663abc..c3e93c2e0a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@ git-annex (10.20260625) UNRELEASED; urgency=medium
     and will attempt to remain buildable with versions of dependencies
     that predate the addition of any LLM generated code.
     See https://git-annex.branchable.com/no_llm_code/
+  * git-annex.cabal: Added NoLLMDependencies build flag.
   * Fix build with time-1.15.
 
  -- Joey Hess <id@joeyh.name>  Wed, 01 Jul 2026 14:12:59 -0400
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index a8aa82ec71..a11f18e350 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -13,14 +13,18 @@ guarantees. Although it would be very much appreciated if they did.
 
 git-annex currently supports being built with versions of
 dependencies that pre-date any introduction of LLM generated code.
-(It is not currently built that way by default but such builds are welcome.)
-Unfortunately, it's not possible to guarantee that will continue to be the
-case in new versions of git-annex. That's the goal, but it may become
-untenable.
-
-Unfortunately, this means that additional work needs to be done to review
-git-annex's dependencies on an ongoing basis to detect the addition of LLM
-generated code.
+To do do, turn on the `NoLLMDependencies` build flag.
+(It is not currently built that way by default, but such builds are welcome.)
+
+Unfortunately, it's not possible to guarantee that will continue to work
+in new versions of git-annex. That's the goal, but it may become
+untenable. It's already the case that building git-annex with the older
+versions of dependencies leaves at least one security hole in a dependency
+open.
+
+This means that additional work needs to be done to review git-annex's
+dependencies on an ongoing basis to detect the addition of LLM generated
+code.
 
 Help with finding these is welcome. Please edit this page and/or file
 bug reports on git-annex if it cannot be built without LLM generated code.
diff --git a/git-annex.cabal b/git-annex.cabal
index 2fef9ae934..d627e0f14c 100644
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -146,6 +146,12 @@ Extra-Source-Files:
   templates/notifications/longpolling.julius
   Utility/libkqueue.h
 
+Flag NoLLMDependencies
+  Description: Avoid building with versions of dependencies that contain 
+    LLM generated code <https://git-annex.branchable.com/no_llm_code/>
+  Default: False
+  Manual: True
+
 Flag Assistant
   Description: Enable git-annex assistant, webapp, and watch command
   Default: True
@@ -213,7 +219,6 @@ custom-setup
 Executable git-annex
   Main-Is: git-annex.hs
   Build-Depends:
-   base (>= 4.18.2.1 && < 5),
    network-uri (>= 2.6),
    optparse-applicative (>= 0.14.2),
    containers (>= 0.5.8),
@@ -252,7 +257,6 @@ Executable git-annex
    conduit,
    time (>= 1.9.1),
    persistent-sqlite (>= 2.13.3),
-   persistent (>= 2.13.3),
    persistent-template (>= 2.8.0),
    unliftio-core,
    microlens,
@@ -277,22 +281,19 @@ Executable git-annex
    tasty-quickcheck,
    tasty-rerun,
    ansi-terminal >= 0.9,
-   aws (>= 0.24.1),
    DAV (>= 1.0),
    network (>= 3.0.0.0),
    network-bsd,
    git-lfs (>= 1.2.0),
    clock (>= 0.3.0),
-   crypton,
-   crypton-connection (>= 0.4.3),
-   crypton-x509-store,
-   tls,
    servant,
    servant-server,
    servant-client,
    servant-client-core,
    warp (>= 3.2.8),
-   warp-tls (>= 3.2.2)
+   warp-tls (>= 3.2.2),
+   crypton-connection (>= 0.4.3),
+   crypton-x509-store
   CC-Options: -Wall
   GHC-Options: -Wall -fno-warn-tabs  -Wincomplete-uni-patterns
   Default-Language: Haskell2010
@@ -300,6 +301,22 @@ Executable git-annex
   Other-Extensions: TemplateHaskell
   -- Some things don't work with the non-threaded RTS.
   GHC-Options: -threaded
+   
+  if flag(NoLLMDependencies)
+    Build-Depends:
+     base (>= 4.18.2.1 && < 4.23),
+     crypton (< 1.1.0),
+     crypton-x509-validation (< 1.9.0),
+     aws (>= 0.24.1) && (< 2.25.3),
+     tls (< 2.3.0),
+     persistent (>= 2.13.3) && (< 2.15.0.0)
+  else
+    Build-Depends:
+     base (>= 4.18.2.1 && < 5),
+     crypton,
+     aws (>= 0.24.1),
+     tls,
+     persistent (>= 2.13.3)
 
   -- Fully optimize for production.
   if flag(Production)
@@ -340,7 +357,10 @@ Executable git-annex
   -- Disabled on arm until this issue is resolved:
   -- https://github.com/k0001/hs-blake3/issues/8
   if flag(Blake3) && (! arch(aarch64) && ! arch(arm))
-    Build-Depends: blake3 (>= 0.3)
+    if flag(NoLLMDependencies)
+      Build-Depends: blake3 (>= 0.3) && (< 0.3.1)
+    else
+      Build-Depends: blake3 (>= 0.3)
     CPP-Options: -DWITH_BLAKE3
     Other-Modules:
       Utility.Hash.Blake3
@@ -366,10 +386,6 @@ Executable git-annex
     CPP-Options: -DWITH_ASSISTANT -DWITH_WEBAPP
     Build-Depends:
       mountpoints,
-      yesod (>= 1.4.3), 
-      yesod-static (>= 1.5.1),
-      yesod-form (>= 1.4.8),
-      yesod-core (>= 1.6.0),
       path-pieces (>= 0.2.1),
       wai,
       wai-extra,
@@ -377,6 +393,18 @@ Executable git-annex
       clientsession,
       template-haskell,
       shakespeare (>= 2.0.11)
+    if flag(NoLLMDependencies)
+      Build-Depends:
+        yesod (>= 1.4.3) && (< 1.7.0.0), 
+        yesod-static (>= 1.5.1) && (<1.6.1.3),
+        yesod-form (>= 1.4.8) && (< 1.7.9.3),
+        yesod-core (>= 1.6.0) && (< 1.7.0.0)
+    else
+      Build-Depends:
+        yesod (>= 1.4.3), 
+        yesod-static (>= 1.5.1),
+        yesod-form (>= 1.4.8),
+        yesod-core (>= 1.6.0)
     Other-Modules:
       Assistant
       Assistant.Alert
diff --git a/stack-botan.yaml b/stack-botan.yaml
index d3e6d2987a..dfa29f07b1 100644
--- a/stack-botan.yaml
+++ b/stack-botan.yaml
@@ -1,5 +1,6 @@
 flags:
   git-annex:
+    NoLLMDependencies: false
     production: true
     parallelbuild: true
     assistant: true
diff --git a/stack.yaml b/stack.yaml
index 6399b77762..323e2aa300 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,5 +1,6 @@
 flags:
   git-annex:
+    NoLLMDependencies: false
     production: true
     parallelbuild: true
     assistant: true

toc
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 6da9209c58..a8aa82ec71 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -27,7 +27,9 @@ bug reports on git-annex if it cannot be built without LLM generated code.
 
 ## known dependencies that contain LLM generated code
 
-## ghc
+[[!toc startlevel=3]]
+
+### ghc
 
 [This commit](https://github.com/ghc/ghc/commit/a5ec467ee3d4e77c026437a545981269acde3434)
 is probably the first, and will be released in the upcoming ghc 9.15.
@@ -65,7 +67,7 @@ But ram is an dependency of other dependencies:
 
 [Reverse dependencies of ram](https://packdeps.haskellers.com/reverse/ram)
 
-## persistent
+### persistent
 
 [persistent](https://hackage.haskell.org/package/persistent) since 2.15.0.0
 
@@ -73,7 +75,7 @@ But ram is an dependency of other dependencies:
 
 git-annex supports being built with older versions.
 
-## yesod
+### yesod
 
 [yesod](https://hackage.haskell.org/package/yesod-core) since 1.7.0.0
 
@@ -84,7 +86,7 @@ and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
 
-## Cabal
+### Cabal
 
 [First LLM generated code](https://github.com/haskell/cabal/commit/da8b314563feb15a3df7bc1baeef4b7aa08f7578)
 

cabal
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 586d2fb285..6da9209c58 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -83,3 +83,11 @@ git-annex supports being built with older versions.
 and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
+
+## Cabal
+
+[First LLM generated code](https://github.com/haskell/cabal/commit/da8b314563feb15a3df7bc1baeef4b7aa08f7578)
+
+Cabal is needed to build git-annex, but is not linked into it.
+There is a risk that a new version of Cabal could need changes to
+git-annex.cabal that prevent an old version building it.

yesod-core version
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index cffabf62ef..586d2fb285 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -75,7 +75,7 @@ git-annex supports being built with older versions.
 
 ## yesod
 
-[yesod](https://hackage.haskell.org/package/yesod), not yet in a released version
+[yesod](https://hackage.haskell.org/package/yesod-core) since 1.7.0.0
 
 [First LLM generated code](https://github.com/yesodweb/yesod/commit/1b033c741ce81d01070de993b285a17e71178156)
 

yesod
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 1fc22aff29..cffabf62ef 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -27,6 +27,20 @@ bug reports on git-annex if it cannot be built without LLM generated code.
 
 ## known dependencies that contain LLM generated code
 
+## ghc
+
+[This commit](https://github.com/ghc/ghc/commit/a5ec467ee3d4e77c026437a545981269acde3434)
+is probably the first, and will be released in the upcoming ghc 9.15.
+
+git-annex remains buildable with older versions of ghc back to 9.6.6.
+
+This will probably prevent git-annex from taking advantage of 
+most new improvements to the Haskell language going forward. 
+That is deeply unfortunate. This is the main reason why git-annex is not
+guaranteed to never change to depend on LLM generated code, because cutting
+it off from all future Haskell language improvements may be worse than the
+alternative.
+
 ### ram and its reverse dependencies
 
 [ram](https://hackage.haskell.org/package/ram) since 0.21.0.
@@ -59,16 +73,13 @@ But ram is an dependency of other dependencies:
 
 git-annex supports being built with older versions.
 
-## ghc
+## yesod
 
-[This commit](https://github.com/ghc/ghc/commit/a5ec467ee3d4e77c026437a545981269acde3434)
-is probably the first, and will be released in the upcoming ghc 9.15.
+[yesod](https://hackage.haskell.org/package/yesod), not yet in a released version
 
-git-annex remains buildable with older versions of ghc back to 9.6.6.
+[First LLM generated code](https://github.com/yesodweb/yesod/commit/1b033c741ce81d01070de993b285a17e71178156)
 
-This will probably prevent git-annex from taking advantage of 
-most new improvements to the Haskell language going forward. 
-That is deeply unfortunate. This is the main reason why git-annex is not
-guaranteed to never change to depend on LLM generated code, because cutting
-it off from all future Haskell language improvements may be worse than the
-alternative.
+[LLM generated commit with a 1489 line commit message](https://github.com/yesodweb/yesod/commit/1ee25122d82f8f94136bf1496a825c6c00b74fcf)
+and 10,000+ lines of changes.
+
+(See [[todo/ditch_yesod]])
diff --git a/doc/todo/ditch_yesod.mdwn b/doc/todo/ditch_yesod.mdwn
index d8ef722ae5..e6c4a4d6e2 100644
--- a/doc/todo/ditch_yesod.mdwn
+++ b/doc/todo/ditch_yesod.mdwn
@@ -14,6 +14,8 @@ reasons:
 * I find that Hamlet does not lend itself to being refactored, everything
   is essentially an IO action with side effects of generating html, so
   can't really bring proper FP tools to bear.
+* Its current maintainer is on the good LLM drugs.
+  <https://github.com/yesodweb/yesod/commit/1ee25122d82f8f94136bf1496a825c6c00b74fcf>
 
 At the moment, servant seems like the nicest place to end up. Just as type
 safe as yesod afaik, and very lightweight and simple and rather awesome.

weaken
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 5848705677..1fc22aff29 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -11,9 +11,12 @@ will never do so.
 However, libraries that git-annex depends on do not generally have such
 guarantees. Although it would be very much appreciated if they did.
 
-So git-annex is also guaranteed to support being built with versions of
+git-annex currently supports being built with versions of
 dependencies that pre-date any introduction of LLM generated code.
 (It is not currently built that way by default but such builds are welcome.)
+Unfortunately, it's not possible to guarantee that will continue to be the
+case in new versions of git-annex. That's the goal, but it may become
+untenable.
 
 Unfortunately, this means that additional work needs to be done to review
 git-annex's dependencies on an ongoing basis to detect the addition of LLM
@@ -65,4 +68,7 @@ git-annex remains buildable with older versions of ghc back to 9.6.6.
 
 This will probably prevent git-annex from taking advantage of 
 most new improvements to the Haskell language going forward. 
-That is deeply unfortunate.
+That is deeply unfortunate. This is the main reason why git-annex is not
+guaranteed to never change to depend on LLM generated code, because cutting
+it off from all future Haskell language improvements may be worse than the
+alternative.

reword
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index c41fd6e185..5848705677 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -63,5 +63,6 @@ is probably the first, and will be released in the upcoming ghc 9.15.
 
 git-annex remains buildable with older versions of ghc back to 9.6.6.
 
-It's deeply unfortunate that git-annex will not be able to take advantage
-of any new improvements to the Haskell language going forward.
+This will probably prevent git-annex from taking advantage of 
+most new improvements to the Haskell language going forward. 
+That is deeply unfortunate.

sadness
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index fbc54586cf..c41fd6e185 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -62,3 +62,6 @@ git-annex supports being built with older versions.
 is probably the first, and will be released in the upcoming ghc 9.15.
 
 git-annex remains buildable with older versions of ghc back to 9.6.6.
+
+It's deeply unfortunate that git-annex will not be able to take advantage
+of any new improvements to the Haskell language going forward.

ghc 9.15
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index da50b14a47..fbc54586cf 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -58,5 +58,7 @@ git-annex supports being built with older versions.
 
 ## ghc
 
-Need to look up the version, but in any case git-annex remains buildable
-with older versions of ghc back to 9.6.6.
+[This commit](https://github.com/ghc/ghc/commit/a5ec467ee3d4e77c026437a545981269acde3434)
+is probably the first, and will be released in the upcoming ghc 9.15.
+
+git-annex remains buildable with older versions of ghc back to 9.6.6.

LLM dependency tracking
diff --git a/CHANGELOG b/CHANGELOG
index b85b0ba223..24f2187984 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
 git-annex (10.20260625) UNRELEASED; urgency=medium
 
+  * git-annex is guaranteed to not contain LLM generated code,
+    and to remain buildable with versions of dependencies that
+    predate the addition of any LLM generated code.
+    See https://git-annex.branchable.com/no_llm_code/
   * Fix build with time-1.15.
 
  -- Joey Hess <id@joeyh.name>  Wed, 01 Jul 2026 14:12:59 -0400
diff --git a/doc/future_proofing.mdwn b/doc/future_proofing.mdwn
index 84883d060f..8be7197eb4 100644
--- a/doc/future_proofing.mdwn
+++ b/doc/future_proofing.mdwn
@@ -51,3 +51,5 @@ problem:
   a remote, as long as you have access to the encryption keys (which
   for some types of encryption are stored in the git-annex branch of
   the repository, sometimes encrypted with your gpg key).
+
+* [[No LLM code]]
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
new file mode 100644
index 0000000000..da50b14a47
--- /dev/null
+++ b/doc/no_llm_code.mdwn
@@ -0,0 +1,62 @@
+LLM generated code in free software is a potential landmine. The copyright
+of such code is an open question, and any current answer to that question
+risks changing at some point in the future.
+
+This is a particular problem for git-annex since [[future proofing]] is an
+important aspect of its design.
+
+So, git-annex does not contain code generated by LLMs and guarantees it
+will never do so.
+
+However, libraries that git-annex depends on do not generally have such
+guarantees. Although it would be very much appreciated if they did.
+
+So git-annex is also guaranteed to support being built with versions of
+dependencies that pre-date any introduction of LLM generated code.
+(It is not currently built that way by default but such builds are welcome.)
+
+Unfortunately, this means that additional work needs to be done to review
+git-annex's dependencies on an ongoing basis to detect the addition of LLM
+generated code.
+
+Help with finding these is welcome. Please edit this page and/or file
+bug reports on git-annex if it cannot be built without LLM generated code.
+
+## known dependencies that contain LLM generated code
+
+### ram and its reverse dependencies
+
+[ram](https://hackage.haskell.org/package/ram) since 0.21.0.
+  
+Note particularly [large LLM generated code churn](https://github.com/jappeace/ram/commit/3a0c034648f1cb7e60e96a681fd74066ff5944fe)
+with apparently broken (how?) changes in 0.21.0 being reverted in
+0.21.1.
+
+Rather than depend on ram, git-annex continues to depend on the unmaintained
+[memory](//hackage.haskell.org/package/memory). 
+Since git-annex's use of this package is fairly incidental, this
+dependency could be removed if it becomes a problem.
+
+But ram is an dependency of other dependencies:
+
+* crypton since 1.1.0
+* crypton-x509-validation since 1.9.0. Note that 1.9.1 fixes 
+  [CVE-2026-9648](https://www.cve.org/CVERecord?id=CVE-2026-9648).
+* aws since 0.25.3 (needed to get the CVE-2026-9648 fix)
+* tls since 2.3.0
+* blake3 since 0.3.1 (optional git-annex dependency)
+
+[Reverse dependencies of ram](https://packdeps.haskellers.com/reverse/ram)
+
+## persistent
+
+[persistent](https://hackage.haskell.org/package/persistent) since 2.15.0.0
+
+[First LLM generated code](https://github.com/yesodweb/persistent/commit/ac0a8698f38ae3b07acdacf0ce236d7fc73bb077)
+
+git-annex supports being built with older versions.
+
+## ghc
+
+Need to look up the version, but in any case git-annex remains buildable
+with older versions of ghc back to 9.6.6.

comment
diff --git a/doc/bugs/auth/comment_10_5cfac94f32b17336a81deabe2508c129._comment b/doc/bugs/auth/comment_10_5cfac94f32b17336a81deabe2508c129._comment
new file mode 100644
index 0000000000..50c81dfbb9
--- /dev/null
+++ b/doc/bugs/auth/comment_10_5cfac94f32b17336a81deabe2508c129._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 10"""
+ date="2026-07-01T20:12:11Z"
+ content="""
+`git-annex push` won't always trigger this problem because often it has
+things to `git push` to the server before sending content. A more reliable
+way is to use `git-annex copy --to`.
+"""]]

comment
diff --git a/doc/bugs/auth.mdwn b/doc/bugs/auth.mdwn
index 170aa3be31..c45cea90ba 100644
--- a/doc/bugs/auth.mdwn
+++ b/doc/bugs/auth.mdwn
@@ -1,3 +1,6 @@
+[[!meta title="git-credential-oauth does not work with git-annex push to
+git-annex p2phttp behind forgejo-aneksajo unless git push primes the credential cache first"]]
+
 ### Please describe the problem.
 
 `git annex push` does not trigger git credential retrieval reliably, but forces manual credential entry.
diff --git a/doc/bugs/auth/comment_9_c5b7872812b6aa29224f45b266a7e87e._comment b/doc/bugs/auth/comment_9_c5b7872812b6aa29224f45b266a7e87e._comment
new file mode 100644
index 0000000000..08738e4aa6
--- /dev/null
+++ b/doc/bugs/auth/comment_9_c5b7872812b6aa29224f45b266a7e87e._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 9"""
+ date="2026-07-01T20:00:36Z"
+ content="""
+`git-annex push` is talking to `git-annex p2phttp` which is on a
+`Forgejo-aneksajo` server.
+
+In this case, the server responds with HTTP 401, and I have verified that
+response does *not* include the `www-authenticate` header.
+
+So, git-annex's code to send that header on to `git-credential` seems fine,
+and this needs to be fixed in either p2phttp or Forgejo-aneksajo to make
+the header be sent. I suspect it will need to be done in Forgejo-aneksajo.
+"""]]

comment
diff --git a/doc/bugs/auth/comment_8_d6b94d6eef80a8711899f008bb31609b._comment b/doc/bugs/auth/comment_8_d6b94d6eef80a8711899f008bb31609b._comment
new file mode 100644
index 0000000000..e56517d8ac
--- /dev/null
+++ b/doc/bugs/auth/comment_8_d6b94d6eef80a8711899f008bb31609b._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 8"""
+ date="2026-07-01T19:40:42Z"
+ content="""
+The wwwauth field is the relevant one.
+
+There was a past bug about a very similar problem:
+<https://git-annex.branchable.com/bugs/__96__git_annex_push__96___does_not_use_git-credential-oauth/>
+
+[[!commit 32fac0b103db1982b748f15def825e1490d34f50]] was supposed to fix
+that, by passing the www-authenticate http header in to git credential.
+
+This bug must be a case where that doesn't happen.
+"""]]

comment
diff --git a/doc/bugs/auth/comment_7_fcb6f9dacdae34ee522d0efd53038eed._comment b/doc/bugs/auth/comment_7_fcb6f9dacdae34ee522d0efd53038eed._comment
new file mode 100644
index 0000000000..54f2b67f4e
--- /dev/null
+++ b/doc/bugs/auth/comment_7_fcb6f9dacdae34ee522d0efd53038eed._comment
@@ -0,0 +1,22 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 7"""
+ date="2026-07-01T19:34:30Z"
+ content="""
+@mih told me the url privately. (There is only 1.)
+
+It is indeed a host which when passed to `git-credential-oauth get`
+it decides not to do anything.
+
+But, `git push` to the same url uses `git-credential-oauth get` and it
+*does* do oauth.
+
+Dumping what git sends to it, there are additional fields beyond what
+git-annex sends:
+
+	capability[]=authtype
+	capability[]=state
+	protocol=https
+	host=<obscured>
+	wwwauth[]=Basic realm="Gitea"
+"""]]

Fix build with time-1.15
It added a Seconds pattern, so rename Seconds to SecondsDelay to avoid
conflict. Also renamed Microseconds to MicrosecondsDelay; although time
does not have Microseconds, it has Picoseconds, so might add Microseconds
later..
diff --git a/Annex/Init.hs b/Annex/Init.hs
index e610fbce00..7966ce7595 100644
--- a/Annex/Init.hs
+++ b/Annex/Init.hs
@@ -428,7 +428,7 @@ probeLockSupport = withEventuallyCleanedOtherTmp $ \tmp -> do
 		return ok
 	
 	warnstall annexrunner = do
-		threadDelaySeconds (Seconds 10)
+		threadDelaySeconds (SecondsDelay 10)
 		annexrunner $ do
 			warning "Probing the filesystem for POSIX fcntl lock support is taking a long time."
 			warning "(Setting annex.pidlock will avoid this probe.)"
diff --git a/Annex/StallDetection.hs b/Annex/StallDetection.hs
index 9b885c2ecf..9e6f67ba76 100644
--- a/Annex/StallDetection.hs
+++ b/Annex/StallDetection.hs
@@ -55,7 +55,7 @@ detectStalls (Just (StallDetection bwrate@(BwRate _minsz duration))) metervar on
 	let BwRate scaledminsz scaledduration = upscale bwrate timepassed
 	detectStalls' scaledminsz scaledduration metervar onstall v
   where
-	minwaitsecs = Seconds $
+	minwaitsecs = SecondsDelay $
 		min 60 (fromIntegral (durationSeconds duration))
 	waitforfirstupdate startval = do
 		liftIO $ threadDelaySeconds minwaitsecs
@@ -75,7 +75,7 @@ detectStalls (Just ProbeStallDetection) metervar onstall = do
   where
 	duration = Duration 60
 
-	delay = Seconds (fromIntegral (durationSeconds duration) `div` 2)
+	delay = SecondsDelay (fromIntegral (durationSeconds duration) `div` 2)
 	
 	waitforfirstupdate startval = do
 		liftIO $ threadDelaySeconds delay
@@ -115,7 +115,7 @@ detectStalls' minsz duration metervar onstall st = do
 			| sofar - prev < minsz -> onstall
 			| otherwise -> cont
   where
-	delay = Seconds (fromIntegral (durationSeconds duration))
+	delay = SecondsDelay (fromIntegral (durationSeconds duration))
 
 readMeterVar
 	:: MonadIO m
diff --git a/Annex/Transfer.hs b/Annex/Transfer.hs
index 7ec629e442..191c7061fa 100644
--- a/Annex/Transfer.hs
+++ b/Annex/Transfer.hs
@@ -360,19 +360,19 @@ forwardRetry numretries old new
  - by git configuration. -}
 configuredRetry :: RetryDecider
 configuredRetry numretries _old new = do
-	(maxretries, Seconds initretrydelay) <- getcfg $ 
+	(maxretries, SecondsDelay initretrydelay) <- getcfg $ 
 		Remote.gitconfig <$> transferRemote new
 	if numretries < maxretries
 		then do
-			let retrydelay = Seconds (initretrydelay * 2^(numretries-1))
-			showSideAction $ UnquotedString $ "Delaying " ++ show (fromSeconds retrydelay) ++ "s before retrying."
+			let retrydelay = SecondsDelay (initretrydelay * 2^(numretries-1))
+			showSideAction $ UnquotedString $ "Delaying " ++ show (fromSecondsDelay retrydelay) ++ "s before retrying."
 			liftIO $ threadDelaySeconds retrydelay
 			return True
 		else return False
   where
 	globalretrycfg = fromMaybe 0 . annexRetry
 		<$> Annex.getGitConfig
-	globalretrydelaycfg = fromMaybe (Seconds 1) . annexRetryDelay
+	globalretrydelaycfg = fromMaybe (SecondsDelay 1) . annexRetryDelay
 		<$> Annex.getGitConfig
 	getcfg Nothing = (,) <$> globalretrycfg <*> globalretrydelaycfg
 	getcfg (Just gc) = (,)
diff --git a/Assistant/CredPairCache.hs b/Assistant/CredPairCache.hs
index eba3d2b779..a645840b15 100644
--- a/Assistant/CredPairCache.hs
+++ b/Assistant/CredPairCache.hs
@@ -27,7 +27,7 @@ import Control.Concurrent
  - Note that repeatedly caching the same CredPair
  - does not reset its expiry time.
  -}
-cacheCred :: CredPair -> Seconds -> Assistant ()
+cacheCred :: CredPair -> SecondsDelay -> Assistant ()
 cacheCred (login, password) expireafter = do
 	cache <- getAssistant credPairCache
 	liftIO $ do
diff --git a/Assistant/Repair.hs b/Assistant/Repair.hs
index caa02138e2..978d59b080 100644
--- a/Assistant/Repair.hs
+++ b/Assistant/Repair.hs
@@ -149,7 +149,7 @@ repairStaleLocks lockfiles = go =<< getsizes
 	go [] = return ()
 	go l = whenM (liftIO $ null <$> Lsof.query ("--" : map (fromOsPath . fst) l)) $ do
 		debug ["Waiting for 60 seconds to check stale git lock file"]
-		liftIO $ threadDelaySeconds $ Seconds 60
+		liftIO $ threadDelaySeconds $ SecondsDelay 60
 		l' <- getsizes
 		if l' == l
 			then liftIO $ mapM_ (removeWhenExistsWith removeFile . fst) l
diff --git a/Assistant/Restart.hs b/Assistant/Restart.hs
index 58a17c12a2..5a3f03b943 100644
--- a/Assistant/Restart.hs
+++ b/Assistant/Restart.hs
@@ -52,7 +52,7 @@ postRestart url = do
 	modifyDaemonStatus_ $ \status -> status { globalRedirUrl = Just url }
 	liftIO . sendNotification . globalRedirNotifier =<< getDaemonStatus
 	void $ liftIO $ forkIO $ do
-		threadDelaySeconds (Seconds 120)
+		threadDelaySeconds (SecondsDelay 120)
 		terminateSelf
 
 terminateSelf :: IO ()
diff --git a/Assistant/Threads/Committer.hs b/Assistant/Threads/Committer.hs
index 7c11ecc05c..210f096a21 100644
--- a/Assistant/Threads/Committer.hs
+++ b/Assistant/Threads/Committer.hs
@@ -59,7 +59,7 @@ commitThread :: NamedThread
 commitThread = namedThread "Committer" $ do
 	havelsof <- liftIO $ inSearchPath "lsof"
 	delayadd <- liftAnnex $
-		fmap Seconds . annexDelayAdd <$> Annex.getGitConfig
+		fmap SecondsDelay . annexDelayAdd <$> Annex.getGitConfig
 	largefilematcher <- liftAnnex largeFilesMatcher
 	annexdotfiles <- liftAnnex $ getGitConfigVal annexDotFiles
 	addunlockedmatcher <- liftAnnex $
@@ -111,7 +111,7 @@ waitChangeTime a = waitchanges 0
   where
 	waitchanges lastcommitsize = do
 		-- Wait one one second as a simple rate limiter.
-		liftIO $ threadDelaySeconds (Seconds 1)
+		liftIO $ threadDelaySeconds (SecondsDelay 1)
 		-- Now, wait until at least one change is available for
 		-- processing.
 		cs <- getChanges
@@ -193,7 +193,7 @@ waitChangeTime a = waitchanges 0
 		loop 0 = continue oldchanges
 		loop n = do
 			liftAnnex noop -- ensure Annex state is free
-			liftIO $ threadDelaySeconds (Seconds 1)
+			liftIO $ threadDelaySeconds (SecondsDelay 1)
 			changes <- getAnyChanges
 			if null changes
 				then loop (n - 1)
@@ -280,7 +280,7 @@ commitStaged msg = do
  - Any pending adds that are not ready yet are put back into the ChangeChan,
  - where they will be retried later.
  -}
-handleAdds :: OsPath -> Bool -> GetFileMatcher -> Bool -> Maybe AddUnlockedMatcher -> Maybe Seconds -> [Change] -> Assistant [Change]
+handleAdds :: OsPath -> Bool -> GetFileMatcher -> Bool -> Maybe AddUnlockedMatcher -> Maybe SecondsDelay -> [Change] -> Assistant [Change]
 handleAdds lockdowndir havelsof largefilematcher annexdotfiles addunlockedmatcher delayadd cs = returnWhen (null incomplete) $ do
 	let (pending, inprocess) = partition isPendingAddChange incomplete
 	let lockdownconfig = LockDownConfig
@@ -467,7 +467,7 @@ handleAdds lockdowndir havelsof largefilematcher annexdotfiles addunlockedmatche
  -
  - Check by running lsof on the repository.
  -}
-safeToAdd :: OsPath -> LockDownConfig -> Bool -> Maybe Seconds -> [Change] -> [Change] -> Assistant [Either Change Change]
+safeToAdd :: OsPath -> LockDownConfig -> Bool -> Maybe SecondsDelay -> [Change] -> [Change] -> Assistant [Either Change Change]
 safeToAdd _ _ _ _ [] [] = return []
 safeToAdd lockdowndir lockdownconfig havelsof delayadd pending inprocess = do
 	maybe noop (liftIO . threadDelaySeconds) delayadd
diff --git a/Assistant/Threads/ConfigMonitor.hs b/Assistant/Threads/ConfigMonitor.hs
index 97cd4af8bb..9a251616c5 100644
--- a/Assistant/Threads/ConfigMonitor.hs
+++ b/Assistant/Threads/ConfigMonitor.hs
@@ -50,7 +50,7 @@ configMonitorThread = namedThread "ConfigMonitor" $ loop =<< getConfigs
 			{- Record a commit to get this config
 			 - change pushed out to remotes. -}
 			recordCommit
-		liftIO $ threadDelaySeconds (Seconds 60)
+		liftIO $ threadDelaySeconds (SecondsDelay 60)
 		loop new
 
 {- Config files, and their checksums. -}
diff --git a/Assistant/Threads/Cronner.hs b/Assistant/Threads/Cronner.hs
index 9b063b5882..21282e0291 100644
--- a/Assistant/Threads/Cronner.hs
+++ b/Assistant/Threads/Cronner.hs
@@ -116,8 +116,8 @@ sleepingActivityThread urlrenderer activity lasttime = go lasttime =<< getnextti
 	desc = fromScheduledActivity activity
 	schedule = getSchedule activity
 	waitrun l t mmaxt = do
-		seconds <- liftIO $ secondsUntilLocalTime t
-		when (seconds > Seconds 0) $ do
+		seconds <- liftIO $ secondsDelayUntilLocalTime t
+		when (seconds > SecondsDelay 0) $ do
 			debug ["waiting", show seconds, "for next scheduled", desc]
 			liftIO $ threadDelaySeconds seconds
 		now <- liftIO getCurrentTime
@@ -161,14 +161,14 @@ remoteActivityThread urlrenderer mvar activity lasttime = do
 	go _ = noop -- running at exact time not handled here
 	loop = remoteActivityThread urlrenderer mvar activity
 
-secondsUntilLocalTime :: LocalTime -> IO Seconds
-secondsUntilLocalTime t = do
+secondsDelayUntilLocalTime :: LocalTime -> IO SecondsDelay
+secondsDelayUntilLocalTime t = do
 	now <- getCurrentTime
 	tz <- getTimeZone now
 	let secs = truncate $ diffUTCTime (localTimeToUTC tz t) now
 	return $ if secs > 0

(Diff truncated)
formatting
diff --git a/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment b/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment
index b6eabb917d..1aaf96e8de 100644
--- a/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment
+++ b/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment
@@ -4,8 +4,8 @@
  date="2026-07-01T17:00:06Z"
  content="""
 In your bug report I noticed that the password prompt is for
-"https://oauth2@<host>" but when you git push it's to
-"https://<host>/<repo>.git". I'm not clear on if these were
+`"https://oauth2@<host>"` but when you git push it's to
+`"https://<host>/<repo>.git"`. I'm not clear on if these were
 really different urls or if it was a mistake in your sanitization.
 
 If there are different urls, `git-credential-oauth` could have acted

comment
diff --git a/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment b/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment
new file mode 100644
index 0000000000..b6eabb917d
--- /dev/null
+++ b/doc/bugs/auth/comment_6_896c29667ae2774d65caafd2a4472e60._comment
@@ -0,0 +1,30 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 6"""
+ date="2026-07-01T17:00:06Z"
+ content="""
+In your bug report I noticed that the password prompt is for
+"https://oauth2@<host>" but when you git push it's to
+"https://<host>/<repo>.git". I'm not clear on if these were
+really different urls or if it was a mistake in your sanitization.
+
+If there are different urls, `git-credential-oauth` could have acted
+on one and not on the other. Although I don't understand why running
+`git pull` would then cache a credential that would be used on a different url.
+
+Assuming you're using `git-credential-oauth` from eg the Debian package of
+that name, here's an example of it not acting on an url:
+
+	joey@darkstar:~/tmp/x>(echo 'protocol=https'; echo 'host=example.com'; echo 'username=oauth2') | git-credential-oauth get
+	joey@darkstar:~/tmp/x>
+
+Running it the same way with github.com doesn't behave that way:
+
+	joey@darkstar:~/tmp/x>(echo 'protocol=https'; echo 'host=github.com'; echo 'username=oauth2') | git-credential-oauth get
+	Please complete authentication in your browser...
+
+This behavior is not network dependent. In fact, `git-credential-oauth` has a
+pile of special casing for different hosts and different servers, including a
+looksLikeForgejo case. If it does not recognize the server name, it doesn't do
+anything without further configuration.
+"""]]

comment
diff --git a/doc/bugs/auth/comment_5_d8f1045d7fe42f74e10766d791bcbfd4._comment b/doc/bugs/auth/comment_5_d8f1045d7fe42f74e10766d791bcbfd4._comment
new file mode 100644
index 0000000000..9298a5fbf1
--- /dev/null
+++ b/doc/bugs/auth/comment_5_d8f1045d7fe42f74e10766d791bcbfd4._comment
@@ -0,0 +1,36 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2026-07-01T15:52:14Z"
+ content="""
+The "Password for" prompt comes from git-credential, not git-annex.
+
+I was able to reproduce that behavior using a `git-credential-foo`
+"helper" which just exits successfully but does not output any credentials.
+With the git config adjusted to use that (as well as cache), git runs
+it but then falls back to the password prompt:
+
+	joey@darkstar:~/tmp/a1>GIT_TRACE=2 git push foo
+	12:08:19.869469 git.c:502               trace: built-in: git push foo
+	12:08:19.869942 run-command.c:673       trace: run_command: GIT_DIR=.git git remote-https foo https://github.com/joeyh/gitannexbuilder
+	12:08:19.869965 run-command.c:765       trace: start_command: /usr/lib/git-core/git remote-https foo https://github.com/joeyh/gitannexbuilder
+	12:08:19.873067 git.c:808               trace: exec: git-remote-https foo https://github.com/joeyh/gitannexbuilder
+	12:08:19.873107 run-command.c:673       trace: run_command: git-remote-https foo https://github.com/joeyh/gitannexbuilder
+	12:08:19.873121 run-command.c:765       trace: start_command: /usr/lib/git-core/git-remote-https foo https://github.com/joeyh/gitannexbuilder
+	12:08:20.549858 run-command.c:673       trace: run_command: 'git credential-cache get'
+	12:08:20.549927 run-command.c:765       trace: start_command: /bin/sh -c 'git credential-cache get' 'git credential-cache get'
+	12:08:20.555753 git.c:502               trace: built-in: git credential-cache get
+	12:08:20.556612 run-command.c:673       trace: run_command: 'git credential-foo get'
+	12:08:20.556635 run-command.c:765       trace: start_command: /bin/sh -c 'git credential-foo get' 'git credential-foo get'
+	12:08:20.561465 git.c:808               trace: exec: git-credential-foo get
+	12:08:20.561506 run-command.c:673       trace: run_command: git-credential-foo get
+	12:08:20.561530 run-command.c:765       trace: start_command: /home/joey/bin/git-credential-foo get
+	Username for 'https://github.com':
+
+So it seems likely that `git-credential-oauth` is just choosing not to do
+anything when git-annex runs it, and then git is falling back like that.
+
+Why it would choose to do that I don't know, but I suppose it could be a
+difference in that is sent to it on stdin when called by git-annex vs
+by git.
+"""]]

comment
diff --git a/doc/bugs/auth/comment_4_d62d27ba12c59f1f0b49a2140276b5b4._comment b/doc/bugs/auth/comment_4_d62d27ba12c59f1f0b49a2140276b5b4._comment
new file mode 100644
index 0000000000..74b0b13458
--- /dev/null
+++ b/doc/bugs/auth/comment_4_d62d27ba12c59f1f0b49a2140276b5b4._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 4"""
+ date="2026-07-01T15:43:12Z"
+ content="""
+At the risk of introducing a second red herring, this seems similar to
+[[bugs/git_annex_checkpresentkey_removes_git_credentials]]. But I don't
+think the same, because here `git-annex push` works after `git push` has
+cached a credential, so git-annex is apparently not removing the cached
+credential.
+
+Could you link to the forgejo-aneksajo issue that is/was being dealt with?
+"""]]

diff --git a/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn b/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
index de5c58ee7c..4812411c79 100644
--- a/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
+++ b/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
@@ -1,6 +1,13 @@
 ## Please describe the problem.
 
-Building git-annex with GHC 9.14/time 1.15 fails with error "Ambiguous occurrence ‘Seconds’." in Annex/StallDetection.hs
+Building git-annex with GHC 9.14/time 1.15 fails with error "Ambiguous occurrence ‘Seconds’." in 
+
+- Annex/StallDetection.hs, 
+- Assistant/Threads/Committer.hs, 
+- Assistant/Threads/Cronner.hs, 
+- Assistant/Threads/Exporter.hs
+- Assistant/Threads/Watcher.hs 
+- Command/FuzzTest.hs
 
 time 1.15 adds "Seconds" to Data.Time.Clock, leading to ambiguity. Easiest fix would probably be to hide the export, alternatively use qualified import.
 

diff --git a/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn b/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
index fe1ad87cf9..de5c58ee7c 100644
--- a/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
+++ b/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
@@ -10,9 +10,9 @@ Build git-annex with GHC 9.14
 
 ## What version of git-annex are you using? On what operating system?
 
-OS: Arch Linux\
-Kernel: 7.0.13-arch1-1\
-GHC: 9.14\
+OS: Arch Linux  
+Kernel: 7.0.13-arch1-1  
+GHC: 9.14  
 git-annex: 10.20251215, newer versions are also affected afaict, including the recently released 10.20260624
 
 ## Please provide any additional information below.

diff --git a/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn b/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
new file mode 100644
index 0000000000..fe1ad87cf9
--- /dev/null
+++ b/doc/bugs/time_1.15__58___Ambiguous_occurrence___8216__Seconds__8217__.mdwn
@@ -0,0 +1,69 @@
+## Please describe the problem.
+
+Building git-annex with GHC 9.14/time 1.15 fails with error "Ambiguous occurrence ‘Seconds’." in Annex/StallDetection.hs
+
+time 1.15 adds "Seconds" to Data.Time.Clock, leading to ambiguity. Easiest fix would probably be to hide the export, alternatively use qualified import.
+
+## What steps will reproduce the problem?
+
+Build git-annex with GHC 9.14
+
+## What version of git-annex are you using? On what operating system?
+
+OS: Arch Linux\
+Kernel: 7.0.13-arch1-1\
+GHC: 9.14\
+git-annex: 10.20251215, newer versions are also affected afaict, including the recently released 10.20260624
+
+## Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+[332 of 760] Compiling Annex.StallDetection ( Annex/StallDetection.hs, dist/build/git-annex/git-annex-tmp/Annex/StallDetection.dyn_o )
+Annex/StallDetection.hs:58:23: error: [GHC-87543]
+    Ambiguous occurrence ‘Seconds’.
+    It could refer to
+       either ‘Utility.ThreadScheduler.Seconds’,
+              imported from ‘Utility.ThreadScheduler’ at Annex/StallDetection.hs:21:1-30
+              (and originally defined at Utility/ThreadScheduler.hs:32:19-48),
+           or ‘Data.Time.Clock.Seconds’,
+              imported from ‘Data.Time.Clock’ at Annex/StallDetection.hs:25:1-22
+              (and originally defined in ‘time-1.15:Data.Time.Clock.Internal.DiffTime’).
+   |
+58 |         minwaitsecs = Seconds $
+   |                       ^^^^^^^
+
+Annex/StallDetection.hs:78:17: error: [GHC-87543]
+    Ambiguous occurrence ‘Seconds’.
+    It could refer to
+       either ‘Utility.ThreadScheduler.Seconds’,
+              imported from ‘Utility.ThreadScheduler’ at Annex/StallDetection.hs:21:1-30
+              (and originally defined at Utility/ThreadScheduler.hs:32:19-48),
+           or ‘Data.Time.Clock.Seconds’,
+              imported from ‘Data.Time.Clock’ at Annex/StallDetection.hs:25:1-22
+              (and originally defined in ‘time-1.15:Data.Time.Clock.Internal.DiffTime’).
+   |
+78 |         delay = Seconds (fromIntegral (durationSeconds duration) `div` 2)
+   |                 ^^^^^^^
+
+Annex/StallDetection.hs:118:17: error: [GHC-87543]
+    Ambiguous occurrence ‘Seconds’.
+    It could refer to
+       either ‘Utility.ThreadScheduler.Seconds’,
+              imported from ‘Utility.ThreadScheduler’ at Annex/StallDetection.hs:21:1-30
+              (and originally defined at Utility/ThreadScheduler.hs:32:19-48),
+           or ‘Data.Time.Clock.Seconds’,
+              imported from ‘Data.Time.Clock’ at Annex/StallDetection.hs:25:1-22
+              (and originally defined in ‘time-1.15:Data.Time.Clock.Internal.DiffTime’).
+    |
+118 |         delay = Seconds (fromIntegral (durationSeconds duration))
+    |                 ^^^^^^^
+
+# End of transcript or log.
+"""]]
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+

update
diff --git a/doc/thanks/contributor-list b/doc/thanks/contributor-list
index ed4fd05472..00d0418c93 100644
--- a/doc/thanks/contributor-list
+++ b/doc/thanks/contributor-list
@@ -74,3 +74,4 @@ Gergely Risko,
 Yaroslav Halchenko,
 Oleg Tolmatcev,
 Greg Steuck,
+edef,

add news item for git-annex 10.20260624
diff --git a/doc/news/version_10.20260316.mdwn b/doc/news/version_10.20260316.mdwn
deleted file mode 100644
index 7ca6f4e414..0000000000
--- a/doc/news/version_10.20260316.mdwn
+++ /dev/null
@@ -1,17 +0,0 @@
-git-annex 10.20260316 released with [[!toggle text="these changes"]]
-[[!toggleable text="""  * Added CHECKPRESENT-URL extension to the external special remote protocol.
-  * Fix reversion in previous version that caused auto-initializing of
-    local git remotes that have annex-ignore set.
-  * Fix bug that caused git credential to be rejected when a http request
-    failed for some reason other than 401.
-  * Importing from the directory special remote will no longer add sizes
-    to keys, which overrode backends that generate unsized keys.
-  * Fix retrival from http git remotes of keys with '%' in their names.
-  * Fix behavior when initremote is used with --sameas=
-    combined with --private.
-  * web, S3, git: Fix bugs in checking if content is present on a remote
-    when configuration does not allow accessing it.
-  * httpalso: Fix bugs in handling content not being present on the remote.
-  * adb: Avoid deleting contents of a non-empty directory when
-    removing the last exported file from the directory.
-  * Improve display of http exceptions."""]]
\ No newline at end of file
diff --git a/doc/news/version_10.20260624.mdwn b/doc/news/version_10.20260624.mdwn
new file mode 100644
index 0000000000..7ee7c9425f
--- /dev/null
+++ b/doc/news/version_10.20260624.mdwn
@@ -0,0 +1,24 @@
+git-annex 10.20260624 released with [[!toggle text="these changes"]]
+[[!toggleable text="""  * Added Botan build flag, which speeds up checksumming significantly
+    for most existing hash backends.
+  * Added Blake3 build flag, which is the fastest available
+    cryptographically secure hash.
+    Thanks to edef for implementing this.
+  * When b3sum is in PATH, use it for faster blake3 hashing.
+  * Added XXH3 build flag, which is the fastest available
+    non-cryptographically secure hash, and is a smaller hash
+    than MD5 or SHA1.
+  * Improve handling of synced/master and similar branches, by
+    only creating such branches when necessary to push to a non-bare
+    remote, and by removing the local synced branch once its changes
+    are merged.
+  * put: New command.
+  * move: Fix bug where an interrupted command other than move could
+    be treated as an interrupted move, resulting in dropping content
+    when not allowed by numcopies.
+  * Fix build with http-types-0.12.5
+  * merge, post-receive: Fix bug that prevented merging any synced/
+    branches into the current branch.
+    (Reversion introduced in 10.20260601)
+  * git-annex.cabal: Remove unused dependency on old-locale.
+  * Remove dependencies on securemem and byteable."""]]
\ No newline at end of file

done
diff --git a/doc/todo/BLAKE3_multithreading.mdwn b/doc/todo/BLAKE3_multithreading.mdwn
index e096343e74..75d1ce79ea 100644
--- a/doc/todo/BLAKE3_multithreading.mdwn
+++ b/doc/todo/BLAKE3_multithreading.mdwn
@@ -36,3 +36,5 @@ Supporting BLAKE3 multithreading would not be trivial. It needs mmaped file acce
 and the C++ library oneTBB, which would need to be somehow linked into the haskell library.
 
 Calling out to b3sum when in PATH would be easier.. --[[Joey]]
+
+> [[done]]

When b3sum is in PATH, use it for faster blake3 hashing
b3sum is so much faster that it's worth the pain.
The pain could be avoided if this is implemented:
https://github.com/k0001/hs-blake3/issues/9
There is no progress display possible when using b3sum. But it's so much
faster that it's still worth using.
Disabled incremental verification for blake3. b3sum is not able to use
multithreading when reading from stdin. And piping to it would surely be
slower.
The ugly use of unsafePerformIO in b3sumAvailable is the second place where
a Backend needs to use it. This suggests that there should be a constuctor
in IO for Backend.
Note that incremetal verification is disabled even when b3sum is not in
PATH. It would be possible to check b3sumAvailable, but again that needs
a constructor in IO for Backend. Anyway, probably users of this backend
will install b3sum.
Sponsored-by: Brock Spratlen
diff --git a/Backend/Hash.hs b/Backend/Hash.hs
index 6834229f15..f159b4d2be 100644
--- a/Backend/Hash.hs
+++ b/Backend/Hash.hs
@@ -179,7 +179,7 @@ sameCheckSum key hash
 	backslash = fromIntegral (ord '\\')
 
 checkKeyChecksumIncremental :: HashType -> Maybe (Key -> Annex IncrementalVerifier)
-checkKeyChecksumIncremental hash = case snd (hasher hash) of
+checkKeyChecksumIncremental hash = case hashIncremental (hasher hash) of
 	Just iv -> Just (liftIO . iv)
 	Nothing -> Nothing
 
@@ -238,15 +238,25 @@ trivialMigrate' oldkey newbackend afile maxextlen maxexts
 	newvariety = backendVariety newbackend
 
 hashFile :: HashType -> OsPath -> MeterUpdate -> Annex Hash
-hashFile hash file meterupdate = 
-	liftIO $ withMeteredFile file meterupdate $ \b -> do
-		let h = (fst $ hasher hash) b
+hashFile hashtype file meterupdate = case hashFileFast o of
+	Nothing -> liftIO hashpure
+	Just a -> liftIO $ a file >>= \case
+		Just h -> return h
+		Nothing -> hashpure
+  where
+	o = hasher hashtype
+	hashpure = withMeteredFile file meterupdate $ \b -> do
+		let h = (hashPure o) b
 		-- Force full evaluation of hash so whole file is read
 		-- before returning.
 		evaluate (rnf h)
 		return h
 
-type Hasher = (L.ByteString -> Hash, Maybe (Key -> IO IncrementalVerifier))
+data Hasher = Hasher
+	{ hashPure :: L.ByteString -> Hash
+	, hashFileFast :: Maybe (OsPath -> IO (Maybe Hash))
+	, hashIncremental :: Maybe (Key -> IO IncrementalVerifier)
+	}
 
 hasher :: HashType -> Hasher
 hasher MD5Hash = md5Hasher
@@ -266,10 +276,12 @@ hasher XXH3Hash = xxh3Hasher
 #endif
 
 mkHasher :: (L.ByteString -> HashDigest) -> IO IncrementalHasher -> Hasher
-mkHasher h i = 
-	( digestToHash . h
-	, Just $ mkIncrementalVerifier i descChecksum . sameCheckSum
-	)
+mkHasher h i = Hasher
+	{ hashPure = digestToHash . h
+	, hashFileFast = Nothing
+	, hashIncremental = Just $
+		mkIncrementalVerifier i descChecksum . sameCheckSum
+	}
 
 sha2Hasher :: HashSize -> Hasher
 sha2Hasher (HashSize hashsize)
@@ -322,12 +334,22 @@ blake2spHasher (HashSize hashsize)
 
 #ifdef WITH_BLAKE3
 blake3Hasher :: Hasher
-blake3Hasher = (blake3, Just $ blake3IncrementalVerifier descChecksum . sameCheckSum)
+blake3Hasher = Hasher
+	{ hashPure = blake3
+	, hashFileFast = Just blake3File
+	-- No incremental verification for blake3 because
+	-- blake3File is much faster.
+	, hashIncremental = Nothing
+	}
 #endif
 
 #ifdef WITH_XXH3
 xxh3Hasher :: Hasher
-xxh3Hasher = (digestToHash . xxh3, xxh3Incremental)
+xxh3Hasher = Hasher
+	{ hashPure = digestToHash . xxh3
+	, hashFileFast = Nothing
+	, hashIncremental = xxh3Incremental
+	}
 #endif
 
 sha1Hasher :: Hasher
@@ -367,6 +389,6 @@ testKeyHash = SHA2Hash (HashSize 256)
 genTestKey :: L.ByteString -> Key
 genTestKey content = addTestE $ mkKey $ \kd -> kd
 	{ keyName = S.toShort $ hashByteString $
-		(fst $ hasher testKeyHash) content
+		(hashPure $ hasher testKeyHash) content
 	, keyVariety = backendVariety testKeyBackend
 	}
diff --git a/CHANGELOG b/CHANGELOG
index ebb6c708ac..d54f23e8b0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ git-annex (10.20260602) UNRELEASED; urgency=medium
   * Added Blake3 build flag, which is the fastest available
     cryptographically secure hash.
     Thanks to edef for implementing this.
+  * When b3sum is in PATH, use it for faster blake3 hashing.
   * Added XXH3 build flag, which is the fastest available
     non-cryptographically secure hash, and is a smaller hash
     than MD5 or SHA1.
diff --git a/Utility/Hash/Blake3.hs b/Utility/Hash/Blake3.hs
index 99eb510cd9..5b6c6659d5 100644
--- a/Utility/Hash/Blake3.hs
+++ b/Utility/Hash/Blake3.hs
@@ -1,4 +1,4 @@
-{- Blake3 convenience wrapper.
+{- Blake3 convenience wrapper with b3hash support.
  -
  - Copyright 2026 Joey Hess <id@joeyh.name>
  - Copyright 2022 edef <edef@edef.eu>
@@ -12,16 +12,25 @@
 module Utility.Hash.Blake3 (
 	blake3,
 	blake3IncrementalVerifier,
+	blake3File,
 ) where
 
 import Utility.Hash.Types
 import Utility.Hash.Incremental
 import Utility.FileSystemEncoding
+import Utility.OsPath
+import Utility.Path
+import Utility.FileSize
+import Utility.Process
+import Utility.Exception
+import Utility.Monad
 
 import qualified BLAKE3
 import qualified Data.ByteString as S
 import qualified Data.ByteString.Lazy as L
 import Data.IORef
+import Control.Concurrent.MVar
+import GHC.IO (unsafePerformIO)
 
 finalize :: BLAKE3.Hasher -> BLAKE3.Digest BLAKE3.DEFAULT_DIGEST_LEN
 finalize = BLAKE3.finalize
@@ -46,3 +55,49 @@ blake3IncrementalVerifier desc samechecksum = do
 		, positionIncrementalVerifier = fmap snd <$> readIORef v
 		, descIncrementalVerifier = desc
 		}
+
+{- When b3sum is in the path, this uses it to hash a file. For large
+ - files, that is much faster due to supporting parallelism.
+ -
+ - This returns Nothing when b3sum is not in the path, or exits nonzero, 
+ - or when the file is too small to be worth running it.
+ -}
+blake3File :: OsPath -> IO (Maybe Hash)
+blake3File file = takeMVar b3sumAvailable >>= \case
+	Just True -> do
+		putMVar b3sumAvailable (Just True)
+		runb3sum
+	Just False -> do
+		putMVar b3sumAvailable (Just False)
+		return Nothing
+	Nothing -> ifM (inSearchPath "b3sum")
+		( do
+			putMVar b3sumAvailable (Just True)
+			runb3sum
+		, do
+			putMVar b3sumAvailable (Just False)
+			return Nothing
+		)
+  where
+	runb3sum = ifM filelargeenough
+		( tryNonAsync runb3sum' >>= return . \case
+			Right output -> case lines output of
+				(hash:[]) | length hash == 64 ->
+					Just $ Hash $ encodeBS hash
+				_ -> Nothing
+			Left _ ->  Nothing
+		, return Nothing
+		)
+	runb3sum' = readProcess "b3sum" ["--no-names", "--", fromOsPath file]
+	
+	-- A file needs to be about 3mb in size before the overhead of
+	-- starting a b3sum process is worthwhile.
+	filelargeenough = tryNonAsync (getFileSize file) >>= return . \case
+		Right sz -> sz > 3000000
+		Left _ -> False
+
+{- Used to avoid needing to check the PATH for b3sum each time
+ - blake3File is called. -}
+{-# NOINLINE b3sumAvailable #-}
+b3sumAvailable :: MVar (Maybe Bool)
+b3sumAvailable = unsafePerformIO $ newMVar Nothing
diff --git a/debian/control b/debian/control
index 321eb45783..e368394333 100644
--- a/debian/control
+++ b/debian/control
@@ -121,6 +121,7 @@ Recommends:
 	git-remote-gcrypt (>= 0.20130908-6),
 	nocache,
 	aria2,
+	b3sum,

(Diff truncated)
update
diff --git a/doc/todo/BLAKE3_multithreading.mdwn b/doc/todo/BLAKE3_multithreading.mdwn
index de60122a4f..e096343e74 100644
--- a/doc/todo/BLAKE3_multithreading.mdwn
+++ b/doc/todo/BLAKE3_multithreading.mdwn
@@ -29,6 +29,9 @@ file with SHA256SUM, git-annex fsck:
 	11.18user 2.11system 0:13.41elapsed 99%CPU (0avgtext+0avgdata 53792maxresident)k
 	7816912inputs+288outputs (2major+8615minor)pagefaults 0swaps
 
+Using -J with enough concurrent files might approach b3sum's speed,
+but often a user wants to add or fsck 1 file as fast as possible.
+
 Supporting BLAKE3 multithreading would not be trivial. It needs mmaped file access,
 and the C++ library oneTBB, which would need to be somehow linked into the haskell library.
 

todo
diff --git a/doc/todo/BLAKE3_multithreading.mdwn b/doc/todo/BLAKE3_multithreading.mdwn
new file mode 100644
index 0000000000..de60122a4f
--- /dev/null
+++ b/doc/todo/BLAKE3_multithreading.mdwn
@@ -0,0 +1,35 @@
+git-annex using BLAKE3 is significantly slower than b3sum.
+The latter uses BLAKE3's multithreading, which is not available in the
+haskell library.
+
+<https://github.com/k0001/hs-blake3/issues/9>
+
+On a 4 gb file:
+
+git-annex fsck:
+
+	1.25user 2.36system 0:07.81elapsed 46%CPU (0avgtext+0avgdata 57676maxresident)k
+	5539480inputs+208outputs (2major+9161minor)pagefaults 0swaps
+
+b3sum:
+
+	1.60user 3.03system 0:02.98elapsed 155%CPU (0avgtext+0avgdata 3158216maxresident)k
+	7013984inputs+0outputs (15345major+57763minor)pagefaults 0swaps
+
+b3sum --num-threads 1:
+
+	1.24user 1.62system 0:07.31elapsed 39%CPU (0avgtext+0avgdata 2326392maxresident)k
+	5902456inputs+0outputs (3262major+43283minor)pagefaults 0swaps
+
+A massive amount of performance is being left on the table.
+
+Although even so, BLAKE3 is still signficantly faster than SHA2, eg on the same
+file with SHA256SUM, git-annex fsck:
+
+	11.18user 2.11system 0:13.41elapsed 99%CPU (0avgtext+0avgdata 53792maxresident)k
+	7816912inputs+288outputs (2major+8615minor)pagefaults 0swaps
+
+Supporting BLAKE3 multithreading would not be trivial. It needs mmaped file access,
+and the C++ library oneTBB, which would need to be somehow linked into the haskell library.
+
+Calling out to b3sum when in PATH would be easier.. --[[Joey]]

done
diff --git a/doc/todo/add_xxHash_backend.mdwn b/doc/todo/add_xxHash_backend.mdwn
index cf9143ea09..065d41ade1 100644
--- a/doc/todo/add_xxHash_backend.mdwn
+++ b/doc/todo/add_xxHash_backend.mdwn
@@ -1 +1,3 @@
 From https://cyan4973.github.io/xxHash/ , xxHash seems much faster than md5 with comparable quality.  There's a Haskell implementation.
+
+> [[done]] (XX3) --[[Joey]]

XXH3
This is the fastest available non-cryptographically secure hash,
and is a smaller hash than MD5 or SHA1.
Had to work around a number of issues with the haskell library.
And it is not available on 32 bit systems until one of those issues is
fixed.
Sponsored-by: Jack Hill
diff --git a/Backend/Hash.hs b/Backend/Hash.hs
index 80b223f838..50eadc1bb2 100644
--- a/Backend/Hash.hs
+++ b/Backend/Hash.hs
@@ -41,6 +41,13 @@ import Control.Exception (evaluate)
 import Data.IORef
 import qualified BLAKE3
 #endif
+#ifdef WITH_XXH3
+import qualified Data.Digest.XXHash.FFI as XXH3
+import qualified Data.Hashable as Hashable
+import Data.Word
+import Data.Bits
+import Data.ByteString.Builder
+#endif
 
 data HashType
 	= MD5Hash
@@ -55,6 +62,9 @@ data HashType
 #ifdef WITH_BLAKE3
 	| Blake3Hash
 #endif
+#ifdef WITH_XXH3
+	| XXH3Hash
+#endif
 
 cryptographicallySecure :: HashType -> Bool
 cryptographicallySecure (SHA2Hash _) = True
@@ -67,6 +77,9 @@ cryptographicallySecure (Blake2spHash _) = True
 #ifdef WITH_BLAKE3
 cryptographicallySecure Blake3Hash = True
 #endif
+#ifdef WITH_XXH3
+cryptographicallySecure XXH3Hash = False
+#endif
 cryptographicallySecure SHA1Hash = False
 cryptographicallySecure MD5Hash = False
 
@@ -85,6 +98,11 @@ hashes = concat
 	, map (Blake2spHash . HashSize) [256, 224]
 #ifdef WITH_BLAKE3
 	, [Blake3Hash]
+#endif
+#ifdef WITH_XXH3
+	, if xxH3Supported
+		then [XXH3Hash]
+		else []
 #endif
 	, [SHA1Hash]
 	, [MD5Hash]
@@ -99,7 +117,7 @@ genBackend hash = Backend
 	{ backendVariety = hashKeyVariety hash (HasExt False)
 	, genKey = Just (keyValue hash)
 	, verifyKeyContent = Just $ checkKeyChecksum sameCheckSum hash
-	, verifyKeyContentIncrementally = Just $ checkKeyChecksumIncremental hash
+	, verifyKeyContentIncrementally = checkKeyChecksumIncremental hash
 	, canUpgradeKey = Just needsUpgrade
 	, fastMigrate = Just trivialMigrate
 	, isStableKey = const True
@@ -127,6 +145,9 @@ hashKeyVariety (Blake2spHash size) he = Blake2spKey size he
 #ifdef WITH_BLAKE3
 hashKeyVariety Blake3Hash he = Blake3Key he
 #endif
+#ifdef WITH_XXH3
+hashKeyVariety XXH3Hash he = XXH3Key he
+#endif
 
 {- A key is a hash of its contents. -}
 keyValue :: HashType -> KeySource -> MeterUpdate -> Annex Key
@@ -169,8 +190,10 @@ sameCheckSum key hash
 	expected = keyHash key
 	backslash = fromIntegral (ord '\\')
 
-checkKeyChecksumIncremental :: HashType -> Key -> Annex IncrementalVerifier
-checkKeyChecksumIncremental hash key = liftIO $ (snd $ hasher hash) key
+checkKeyChecksumIncremental :: HashType -> Maybe (Key -> Annex IncrementalVerifier)
+checkKeyChecksumIncremental hash = case snd (hasher hash) of
+	Just iv -> Just (liftIO . iv)
+	Nothing -> Nothing
 
 keyHash :: Key -> S.ByteString
 keyHash = fst . splitKeyNameExtension
@@ -235,7 +258,7 @@ hashFile hash file meterupdate =
 		evaluate (rnf h)
 		return h
 
-type Hasher = (L.ByteString -> Hash, Key -> IO IncrementalVerifier)
+type Hasher = (L.ByteString -> Hash, Maybe (Key -> IO IncrementalVerifier))
 
 hasher :: HashType -> Hasher
 hasher MD5Hash = md5Hasher
@@ -250,9 +273,15 @@ hasher (Blake2spHash hashsize) = blake2spHasher hashsize
 #ifdef WITH_BLAKE3
 hasher Blake3Hash = blake3Hasher
 #endif
+#ifdef WITH_XXH3
+hasher XXH3Hash = xxh3Hasher
+#endif
 
 mkHasher :: (L.ByteString -> HashDigest) -> IO IncrementalHasher -> Hasher
-mkHasher h i = (digestToHash . h, mkIncrementalVerifier i descChecksum . sameCheckSum)
+mkHasher h i = 
+	( digestToHash . h
+	, Just $ mkIncrementalVerifier i descChecksum . sameCheckSum
+	)
 
 sha2Hasher :: HashSize -> Hasher
 sha2Hasher (HashSize hashsize)
@@ -305,7 +334,7 @@ blake2spHasher (HashSize hashsize)
 
 #ifdef WITH_BLAKE3
 blake3Hasher :: Hasher
-blake3Hasher = (hash, incremental) where
+blake3Hasher = (hash, Just incremental) where
 	finalize :: BLAKE3.Hasher -> BLAKE3.Digest BLAKE3.DEFAULT_DIGEST_LEN
 	finalize = BLAKE3.finalize
 
@@ -331,6 +360,31 @@ blake3Hasher = (hash, incremental) where
 			}
 #endif
 
+#ifdef WITH_XXH3
+xxh3Hasher :: Hasher
+xxh3Hasher = (hash, incremental) where
+	hash = convcanonical . Hashable.hashWithSalt 0 . XXH3.XXH3
+	-- The haskell library does not support incremental verification
+	-- (without going too low-level to be appropriate here).
+	-- https://github.com/haskell-haskey/xxhash-ffi/issues/7
+	incremental = Nothing
+	-- Convert to XXH3 canonical representation.
+	-- This is unfortunately not exposed by xxhash-ffi so has to
+	-- be re-implemented here.
+	-- See https://github.com/haskell-haskey/xxhash-ffi/issues/8
+	convcanonical = digestToHash . HashDigest 
+		. L.toStrict . toLazyByteString 
+		. word64BE . fromint
+	fromint :: Int -> Word64
+	fromint = fromIntegral
+
+-- Due to use of Int, the xxhash-ffi library is currently only suitable
+-- for use on 64 bit (or higher) systems. 
+-- https://github.com/haskell-haskey/xxhash-ffi/issues/6
+xxH3Supported :: Bool
+xxH3Supported = finiteBitSize (0 :: Int) >= 64
+#endif
+
 sha1Hasher :: Hasher
 sha1Hasher = mkHasher sha1 sha1_hasher
 
diff --git a/BuildFlags.hs b/BuildFlags.hs
index e8f8361b7c..e618492b95 100644
--- a/BuildFlags.hs
+++ b/BuildFlags.hs
@@ -74,6 +74,11 @@ buildFlags = filter (not . null)
 	, "Blake3"
 #else
 #warning Building without Blake3 support.
+#endif
+#ifdef WITH_XXH3
+	, "XXH3"
+#else
+#warning Building without XXH3 support.
 #endif
 	]
 
diff --git a/CHANGELOG b/CHANGELOG
index 731f9b615b..a5da5c0f25 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,9 +2,12 @@ git-annex (10.20260602) UNRELEASED; urgency=medium
 
   * Added Botan build flag, which speeds up checksumming significantly
     for most existing hash backends.
-  * Added Blake3 build flag, which is the fastest available hash in
-    git-annex now.
+  * Added Blake3 build flag, which is the fastest available
+    cryptographically secure hash.
     Thanks to edef for their patches.
+  * Added XXH3 build flag, which is the fastest available
+    non-cryptographically secure hash, and is a smaller hash
+    than MD5 or SHA1.
   * Improve handling of synced/master and similar branches, by
     only creating such branches when necessary to push to a non-bare
     remote, and by removing the local synced branch once its changes
diff --git a/Types/Key.hs b/Types/Key.hs
index f7e9bc5a04..06396415a2 100644
--- a/Types/Key.hs
+++ b/Types/Key.hs
@@ -216,6 +216,9 @@ data KeyVariety
 	| Blake2spKey HashSize HasExt
 #ifdef WITH_BLAKE3
 	| Blake3Key HasExt
+#endif
+#ifdef WITH_XXH3
+	| XXH3Key HasExt
 #endif
 	| SHA1Key HasExt
 	| MD5Key HasExt
@@ -256,6 +259,9 @@ hasExt (Blake2spKey _ (HasExt b)) = b
 #ifdef WITH_BLAKE3

(Diff truncated)
comment
diff --git a/doc/todo/add_xxHash_backend/comment_7_d8b95d234c09d58be8b60bd0f6b4d767._comment b/doc/todo/add_xxHash_backend/comment_7_d8b95d234c09d58be8b60bd0f6b4d767._comment
new file mode 100644
index 0000000000..77894032ea
--- /dev/null
+++ b/doc/todo/add_xxHash_backend/comment_7_d8b95d234c09d58be8b60bd0f6b4d767._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 7"""
+ date="2026-06-22T15:22:32Z"
+ content="""
+This would still be worth doing despite git-annex supporting BLAKE3 now.
+XXHASH is even faster, due to not needing to do the work to be
+cryptographically secure.
+"""]]

comment
diff --git a/doc/backends/comment_36_d903eb4b0207b848a1d92010f7868bb1._comment b/doc/backends/comment_36_d903eb4b0207b848a1d92010f7868bb1._comment
new file mode 100644
index 0000000000..33d4d91de2
--- /dev/null
+++ b/doc/backends/comment_36_d903eb4b0207b848a1d92010f7868bb1._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""Re: combine worm with partial hash"""
+ date="2026-06-22T15:17:46Z"
+ content="""
+It lowers the probability to only files that happen to start with the same
+prefix. There are many cases where 2 versions of a file start with the same
+data and diverge later, think eg a filesystem image where one file has been
+changed. 
+
+So I think this would give a false sense of protection, kind of like wearing
+a steel toed boot but with a gun still pointed at your foot.
+
+It's better to just not use WORM. Modern computers can hash really fast,
+especially if you use BLAKE3.
+"""]]

mention annex.securehashesonly
diff --git a/doc/backends.mdwn b/doc/backends.mdwn
index 6fb6cc9638..9ff00f12be 100644
--- a/doc/backends.mdwn
+++ b/doc/backends.mdwn
@@ -76,7 +76,8 @@ in `.gitattributes`:
 ## non-cryptograpgically secure backends
 
 The backends below do not guarantee cryptographically that the
-content of an annexed file remains unchanged.
+content of an annexed file remains unchanged. git-annex can be configured
+to not allow these, by the `annex.securehashesonly` git-config setting.
 
 * `SHA1`, `SHA1E`, `MD5`, `MD5E` -- Smaller hashes than `SHA256`
    for those who want a checksum but are not concerned about security.

comment
diff --git a/doc/bugs/blake3_hash_support/comment_11_59b43eddd3f187440824ba36ee75ff57._comment b/doc/bugs/blake3_hash_support/comment_11_59b43eddd3f187440824ba36ee75ff57._comment
new file mode 100644
index 0000000000..b0c5a7d67c
--- /dev/null
+++ b/doc/bugs/blake3_hash_support/comment_11_59b43eddd3f187440824ba36ee75ff57._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 11"""
+ date="2026-06-22T14:56:37Z"
+ content="""
+It would be possible to add other blake3 hash lengths, but I have not
+investigated if there is any benefit (eg performance or security) in
+using anything other than `BLAKE3_256`. Will wait to see if anyone comes up
+with a use case for them.
+
+I don't think it will be a problem to adapt the existing code to use them.
+"""]]

fix
diff --git a/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment b/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment
index c5727fe205..8d730f85fe 100644
--- a/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment
+++ b/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment
@@ -3,7 +3,7 @@
  subject="""comment 9"""
  date="2026-06-22T14:25:51Z"
  content="""
-Updated to blake3 and it is indeed even faster: 6.80s
+Updated to blake3-0.3 and it is indeed even faster: 6.80s
 
 Impressive, that's twice as fast as botan's sha256.
 """]]

blake3 merged
diff --git a/doc/bugs/blake3_hash_support.mdwn b/doc/bugs/blake3_hash_support.mdwn
index 1485e79ac3..f5db2b9384 100644
--- a/doc/bugs/blake3_hash_support.mdwn
+++ b/doc/bugs/blake3_hash_support.mdwn
@@ -375,3 +375,5 @@ index 30614e156..0778948fb 100644
      Build-Depends: network-multicast, network-info
      CPP-Options: -DWITH_PAIRING
 """]]
+
+> [[merged|done]] --[[Joey]]
diff --git a/doc/bugs/blake3_hash_support/comment_10_5c3e349536dbb0c76d3df02e92a60c78._comment b/doc/bugs/blake3_hash_support/comment_10_5c3e349536dbb0c76d3df02e92a60c78._comment
new file mode 100644
index 0000000000..eb58ec2361
--- /dev/null
+++ b/doc/bugs/blake3_hash_support/comment_10_5c3e349536dbb0c76d3df02e92a60c78._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 10"""
+ date="2026-06-22T14:49:17Z"
+ content="""
+The above patches *did* have a memory leak.
+
+I've fixed that, and merged to master.
+"""]]

comment
diff --git a/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment b/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment
new file mode 100644
index 0000000000..c5727fe205
--- /dev/null
+++ b/doc/bugs/blake3_hash_support/comment_9_585f9d6f330c22a954b005cae0428e58._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 9"""
+ date="2026-06-22T14:25:51Z"
+ content="""
+Updated to blake3 and it is indeed even faster: 6.80s
+
+Impressive, that's twice as fast as botan's sha256.
+"""]]

note about build flag
diff --git a/doc/backends.mdwn b/doc/backends.mdwn
index 767f1f7eb2..6fb6cc9638 100644
--- a/doc/backends.mdwn
+++ b/doc/backends.mdwn
@@ -62,7 +62,9 @@ in `.gitattributes`:
 * `BLAKE3_256`, `BLAKE3_256E`
   -- Very fast [Blake3 hash](https://github.com/BLAKE3-team/BLAKE3).
   Unlike the Blake2 family, Blake3 is a single variant optimised for many platforms.
-  It is still faster than Blake2 in most cases.
+  It is still faster than Blake2 in most cases.  
+  This is only supported when git-annex is built with the Blake3 build
+  flag.
 
 ### Other
 

update for blake3
Thanks to annexuser64623
diff --git a/doc/backends.mdwn b/doc/backends.mdwn
index 2d1328590b..767f1f7eb2 100644
--- a/doc/backends.mdwn
+++ b/doc/backends.mdwn
@@ -24,6 +24,8 @@ in `.gitattributes`:
 
 ## recommended backends to use
 
+### SHA-2 and SHA-3
+
 * `SHA256E` -- The default backend for new files, combines a 256 bit SHA-2
   hash of the file's content with the file's extension. This allows
   verifying that the file content is right, and can avoid duplicates of
@@ -39,6 +41,9 @@ in `.gitattributes`:
 * `SKEIN512`, `SKEIN512E`, `SKEIN256`, `SKEIN256E`
   -- [Skein hash](http://en.wikipedia.org/wiki/Skein_hash),
   a well-regarded SHA3 hash competition finalist.
+
+### Blake2 and Blake3
+
 * `BLAKE2B160`, `BLAKE2B224`, `BLAKE2B256`, `BLAKE2B384`, `BLAKE2B512`
   `BLAKE2B160E`, `BLAKE2B224E`, `BLAKE2B256E`, `BLAKE2B384E`, `BLAKE2B512E`
   -- Fast [Blake2 hash](https://blake2.net/) variants optimised for 64 bit
@@ -54,6 +59,13 @@ in `.gitattributes`:
   `BLAKE2SP224E`, `BLAKE2SP256E`
   -- Fast [Blake2 hash](https://blake2.net/) variants optimised for 
   8-way CPUs.
+* `BLAKE3_256`, `BLAKE3_256E`
+  -- Very fast [Blake3 hash](https://github.com/BLAKE3-team/BLAKE3).
+  Unlike the Blake2 family, Blake3 is a single variant optimised for many platforms.
+  It is still faster than Blake2 in most cases.
+
+### Other
+
 * `VURL` -- This is like an `URL` (see below) but the content can
   be verified with a cryptographically secure checksum that is
   recorded in the git-annex branch. It's generated when using 

comment
diff --git a/doc/bugs/blake3_hash_support/comment_8_9bc97dc175da01478fb6c39a6fdff538._comment b/doc/bugs/blake3_hash_support/comment_8_9bc97dc175da01478fb6c39a6fdff538._comment
new file mode 100644
index 0000000000..2ae60679cf
--- /dev/null
+++ b/doc/bugs/blake3_hash_support/comment_8_9bc97dc175da01478fb6c39a6fdff538._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 8"""
+ date="2026-06-22T14:05:49Z"
+ content="""
+blake3 does outperform botan's sha256. I benchmarked it at
+8.30s vs 0:14.36s adding a 4 gb file. That's with blake3-0.2. 
+Newer versions of the blake3 library may be faster yet.
+
+Since the above patches no longer cleanly apply, I have committed
+my update of them to the `blake3` branch. My above review items are not
+addressed yet in that branch. I did add Blake3 to BuildFlags.hs though,
+which was a problem I missed in my earlier review.
+
+Also, the haskell blake3 library has gotten a 0.3 release, which
+the code needs to be updated to build with. (I see no need to retain
+support for 0.2.)
+"""]]

Added a comment
diff --git a/doc/bugs/blake3_hash_support/comment_7_443a5e7eb16d1a859cad9b0d5506c7b1._comment b/doc/bugs/blake3_hash_support/comment_7_443a5e7eb16d1a859cad9b0d5506c7b1._comment
new file mode 100644
index 0000000000..1384caec3c
--- /dev/null
+++ b/doc/bugs/blake3_hash_support/comment_7_443a5e7eb16d1a859cad9b0d5506c7b1._comment
@@ -0,0 +1,48 @@
+[[!comment format=mdwn
+ username="annexuser64623"
+ avatar="http://cdn.libravatar.org/avatar/f2247695660ba216740a81d573e6a3cd"
+ subject="comment 7"
+ date="2026-06-19T10:18:02Z"
+ content="""
+Thanks for your work on Blake3.
+I am looking forward to use Blake3-256 as well since it's much faster than sha2-256 on my machine.
+
+Regarding support via Botan:
+I could not find any recent activity concerning Blake3 Support.
+Links:
+[Botan Discussion on Blake3](https://github.com/randombit/botan/discussions/4755)
+and
+[Botan Issue on Blake3](https://github.com/randombit/botan/issues/3054)
+
+Unfortunately, I'm unfamiliar with Haskell and cannot help with the suggested/requested implementation changes.
+Thus, I created a patch for updating the docs:
+
+```
+diff --git a/doc/backends.mdwn b/doc/backends.mdwn
+index 2d1328590b..fbcc9c87d6 100644
+--- a/doc/backends.mdwn
++++ b/doc/backends.mdwn
+@@ -26,2 +26,4 @@ in `.gitattributes`:
+
++### SHA-2 and SHA-3
++
+ * `SHA256E` -- The default backend for new files, combines a 256 bit SHA-2
+@@ -41,2 +43,5 @@ in `.gitattributes`:
+   a well-regarded SHA3 hash competition finalist.
++
++### Blake2 and Blake3
++
+ * `BLAKE2B160`, `BLAKE2B224`, `BLAKE2B256`, `BLAKE2B384`, `BLAKE2B512`
+@@ -56,2 +61,9 @@ in `.gitattributes`:
+   8-way CPUs.
++* `BLAKE3_256`, `BLAKE3_256E`
++  -- Very fast [Blake3 hash](https://github.com/BLAKE3-team/BLAKE3).
++  Unlike the Blake2 family, Blake3 is a single variant optimised for many platforms.
++  It is still faster than Blake2 in most cases.
++
++### Other
++
+ * `VURL` -- This is like an `URL` (see below) but the content can
+```
+
+"""]]

comment
diff --git a/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_4_49ad0811cab9f1565528b202c6b0d953._comment b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_4_49ad0811cab9f1565528b202c6b0d953._comment
new file mode 100644
index 0000000000..b97a49c0ab
--- /dev/null
+++ b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_4_49ad0811cab9f1565528b202c6b0d953._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""Re: annex-security-allow-unverified-downloads"""
+ date="2026-06-12T15:16:21Z"
+ content="""
+The reason downloading from the web special remote with just an URL key is
+allowed is because it avoids the specific security holes that
+annex-security-allow-unverified-downloads was added to deal with.
+"""]]

comment
diff --git a/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_3_cd276dcc85b3793e3b0bc2367e6081cf._comment b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_3_cd276dcc85b3793e3b0bc2367e6081cf._comment
new file mode 100644
index 0000000000..285ed740a3
--- /dev/null
+++ b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_3_cd276dcc85b3793e3b0bc2367e6081cf._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2026-06-12T15:01:37Z"
+ content="""
+The urls are actually being copied to the new key. But `whereis` only displays
+an url when it's located in a remote that contains a key. After the migration,
+git-annex does not think that your special remote contains the new key,
+so it does not display urls that are claimed by that special remote.
+
+For the web, there is a special case that handles this, recording that the
+new key is present in the web special remote. (setUrlPresent does it)
+
+I don't think it makes sense to make that be done for other types of
+special remotes generally. Consider that `git-annex get`
+would request the special remote get the new key. If the special remote
+is a key/value store, the new key is not located on it, so the get would
+fail. Similarly, `git-annex fsck --from specialremote` would detect a
+problem.
+
+Maybe there are other special remotes that are also url based in a way that
+it would make sense to update the location log when migrating a key.
+The bittorrent special remote comes to mind.
+"""]]