Recent changes to this wiki:

Fix buggy handling of a submodule in a linked git worktree
diff --git a/CHANGELOG b/CHANGELOG
index fa47ee9724..b714896fb3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -8,6 +8,7 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
     in non-unicode locales.
   * Fix bug in parsing the P2P protocol that caused breakage when filenames
     contained unicode multibyte space characters.
+  * Fix buggy handling of a submodule in a linked git worktree.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Git/Config.hs b/Git/Config.hs
index 4e72b73be6..17cad284c7 100644
--- a/Git/Config.hs
+++ b/Git/Config.hs
@@ -1,6 +1,6 @@
 {- git repository configuration handling
  -
- - Copyright 2010-2022 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -24,6 +24,7 @@ import qualified Git.Construct
 import Utility.UserInfo
 import Utility.Process.Transcript
 import Utility.Debug
+import qualified Utility.RawFilePath as R
 
 {- Returns a single git config setting, or a fallback value if not set. -}
 get :: ConfigKey -> ConfigValue -> Repo -> ConfigValue
@@ -181,12 +182,20 @@ updateLocation' r l@(Local {}) = do
 	l' <- case getMaybe "core.worktree" r of
 		Nothing -> return l
 		Just (ConfigValue d) -> do
-			{- core.worktree is relative to the gitdir -}
-			top <- absPath (gitdir l)
+			top <- gitdirtop
 			let p = absPathFrom top (toOsPath d)
 			return $ l { worktree = Just p }
 		Just NoConfigValue -> return l
 	return $ r { location = l' }
+  where
+	{- core.worktree is relative to the gitdir. 
+	 - When the gitdir is a symlink, it's relative to the destination
+	 - of the symlink.
+	 -}
+	gitdirtop = catchMaybeIO (R.readSymbolicLink (fromOsPath (gitdir l))) >>= \case
+		Nothing -> absPath (gitdir l)
+		Just linktarget -> absPath $
+			takeDirectory (gitdir l) </> toOsPath linktarget
 updateLocation' r l = return r { location = l }
 
 data ConfigStyle = ConfigList | ConfigNullList
diff --git a/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn
index 866d9d9088..90a437d609 100644
--- a/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn
+++ b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn
@@ -62,3 +62,4 @@ I have tried it across a range of versions.
 
 Yes, lots. It's been working well for me in keeping track of scientific analyses.
 
+> [[fixed|done]] --[[Joey]]
diff --git a/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree/comment_2_24cedc63f261044299a6723f58374747._comment b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree/comment_2_24cedc63f261044299a6723f58374747._comment
new file mode 100644
index 0000000000..f3154a0147
--- /dev/null
+++ b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree/comment_2_24cedc63f261044299a6723f58374747._comment
@@ -0,0 +1,19 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2026-10-30T14:34:26Z"
+ content="""
+As far as I can see, core.worktree is the only config setting that
+is relative to the path of the gitdir.
+
+Changing the gitdir to be where the symlink points to does not seem like a
+good idea, it would affect git-annex code that creates symlinks pointing
+into the gitdir.
+
+Also, `git rev-parse --git-dir` is just ".git" in this situation,
+so even git internally does not follow the symlink manually, except
+presumably when resolving core.worktree.
+
+So, I've fixed this only in the core.worktree handling code in
+updateLocation.
+"""]]

close
diff --git a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4.mdwn b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4.mdwn
index 1841a805e7..f0b61e5465 100644
--- a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4.mdwn
+++ b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4.mdwn
@@ -88,3 +88,5 @@ ewen@basadi:/annex/downloaded/apple/macos/macOS_13-Ventura$
 ### 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)
 
 Yes, git-annex was working fine for both "addurl" and "importfeed" on the September 2026 release, built in a similar manner (local build from Homebrew Formula).  Thanks, it's been very helpful.
+
+> [[done]], and see [[todo/remove_dependency_on_crypon_and_tls]]. --[[Joey]]

Added a comment
diff --git a/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes/comment_2_668fa983e371ed70bd78371a0e64bc67._comment b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes/comment_2_668fa983e371ed70bd78371a0e64bc67._comment
new file mode 100644
index 0000000000..1796e1c641
--- /dev/null
+++ b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes/comment_2_668fa983e371ed70bd78371a0e64bc67._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="matrss"
+ avatar="http://cdn.libravatar.org/avatar/cd1c0b3be1af288012e49197918395f0"
+ subject="comment 2"
+ date="2026-10-30T10:00:06Z"
+ content="""
+> It does work for special remotes, but not for git remotes.
+
+Ah, well that explains why we saw different behavior :)
+
+Just for the record, I don't currently have a use-case for proxies, and the one I have in mind for the future would only involve a special remote that is read-only by itself already, so I don't see any urgency for this issue. Still, good to have it logged.
+"""]]

analysis
diff --git a/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes/comment_1_1d0142f161d6e0f312e445c395a2fd51._comment b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes/comment_1_1d0142f161d6e0f312e445c395a2fd51._comment
new file mode 100644
index 0000000000..3256eab947
--- /dev/null
+++ b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes/comment_1_1d0142f161d6e0f312e445c395a2fd51._comment
@@ -0,0 +1,21 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-30T06:33:34Z"
+ content="""
+It does work for special remotes, but not for git remotes.
+
+I think the reason for the difference is that Annex.Proxy has
+proxyGitRemoteSide and proxySpecialRemoteSide. The former opens a P2P
+connection to the proxied remote's git-annex-shell, and so it does not
+access the remote via a Remote object like the latter does. So the fact
+that the Remote has methods that have been made readonly has no effect.
+
+So probably proxySpecialRemoteSide needs to look at annex-readonly and then
+somehow limit the P2P connection to ServeReadOnly. But I don't think there
+is actually any way to do that, short of modifying git-annex-shell or
+somehow injecting `GIT_ANNEX_SHELL_READONLY` into its environment when
+running it. So supporting it might involve consuming the P2P protocol
+and filtering out writes before relaying it. Unfortunately, that would
+add copying of DATA, so could be a significant slowdown.
+"""]]

another one
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 35a415330e..d24c258839 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -136,6 +136,10 @@ including `tls`.
 
 git-annex supports being built with older versions.
 
+### http-semantics
+
+TODO
+
 ### Cabal
 
 [First LLM generated code](https://github.com/haskell/cabal/commit/da8b314563feb15a3df7bc1baeef4b7aa08f7578)

diff --git a/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn
index 3d98da0969..0f37021928 100644
--- a/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn
+++ b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn
@@ -150,3 +150,6 @@ ok
 """]]
 
 ### 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)
+
+
+[[!tag projects/ICE4]]

diff --git a/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn
new file mode 100644
index 0000000000..3d98da0969
--- /dev/null
+++ b/doc/bugs/annex-readonly_does_not_apply_to_proxied_remotes.mdwn
@@ -0,0 +1,152 @@
+### Please describe the problem in your own words.
+
+The `remote.proxy-target.annex-readonly` configuration does not prevent modifications to the proxy-target remote if it is accessed via a proxy.
+
+### What steps will reproduce the problem?
+
+[[!format sh """
+#!/usr/bin/env bash
+set -euxo pipefail
+
+# Set up an empty proxy target repository
+mkdir target
+cd target
+git init
+git annex init
+cd ..
+
+# Set up the proxying repository
+mkdir proxy
+cd proxy
+git init
+git annex init
+echo test0 > test0.txt
+git annex add .
+git commit -m commit
+# Add proxy target remote, make it readonly, and enable proxying
+git remote add target ../target
+git config remote.target.annex-readonly true
+git config remote.target.annex-proxy true
+git annex updateproxy
+# Check that the readonly configuration is effective
+set +e
+git annex move --to target
+set -e
+cd ..
+
+# Make a clone of the proxying repository
+git clone ssh://$USER@localhost$PWD/proxy clone
+cd clone
+git annex init
+# Add some new annexed content
+echo test1 > test1.txt
+echo test2 > test2.txt
+git annex add .
+git commit -m commit2
+# Modify the proxy target, even though it is configured to be readonly
+git annex move --to origin-target
+git annex move --from origin-target
+cd ..
+"""]]
+
+### What version of git-annex are you using? On what operating system?
+
+```
+$ git annex version
+git-annex version: 10.20261026-g351f69fa638bde0d2f68ee5b8bab109c75945331
+build flags: Assistant Webapp Inotify TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV Servant OsPath Blake3 XXH3 TastyTap
+dependency versions: aws-0.25.2 bloomfilter-2.0.1.3 crypton-1.0.6 DAV-1.3.4 feed-1.3.2.1 ghc-9.10.3 http-client-0.7.19 torrent-10000.1.3 uuid-1.3.16.1 yesod-1.6.2.3
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 BLAKE3_256E BLAKE3_256 XXH3E XXH3 SHA1E SHA1 MD5E MD5 WORM URL GITBUNDLE GITMANIFEST VURL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg rclone hook external compute mask
+operating system: linux x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+```
+
+### 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
+
+$ ./reproducer.bash 
++ mkdir target
++ cd target
++ git init
+Leeres Git-Repository in /home/icg149/Playground/proxy-readonly/target/.git/ initialisiert
++ git annex init
+init  ok
+(recording state in git...)
++ cd ..
++ mkdir proxy
++ cd proxy
++ git init
+Leeres Git-Repository in /home/icg149/Playground/proxy-readonly/proxy/.git/ initialisiert
++ git annex init
+init  ok
+(recording state in git...)
++ echo test0
++ git annex add .
+add test0.txt 
+ok                                
+(recording state in git...)
++ git commit -m commit
+[main (Root-Commit) 27cd0ae] commit
+ 1 file changed, 1 insertion(+)
+ create mode 120000 test0.txt
++ git remote add target ../target
++ git config remote.target.annex-readonly true
++ git config remote.target.annex-proxy true
++ git annex updateproxy
+Started proxying for target
++ set +e
++ git annex move --to target
+move test0.txt (to target...) 
+  this remote is readonly
+failed
+move: 1 failed
++ set -e
++ cd ..
++ git clone ssh://icg149@localhost/home/icg149/Playground/proxy-readonly/proxy clone
+Klone nach 'clone'...
+remote: Objekte aufzählen: 16, fertig.
+remote: Zähle Objekte: 100% (16/16), fertig.
+remote: Komprimiere Objekte: 100% (12/12), fertig.
+remote: Gesamt 16 (Delta 2), Wiederverwendet 0 (Delta 0), Paket wiederverwendet 0 (von 0)
+Empfange Objekte: 100% (16/16), fertig.
+Löse Unterschiede auf: 100% (2/2), fertig.
++ cd clone
++ git annex init
+init  ok
+(recording state in git...)
++ echo test1
++ echo test2
++ git annex add .
+add test1.txt 
+ok                                
+add test2.txt 
+ok                                
+(recording state in git...)
++ git commit -m commit2
+[main 6a46897] commit2
+ 2 files changed, 2 insertions(+)
+ create mode 120000 test1.txt
+ create mode 120000 test2.txt
++ git annex move --to origin-target
+move test1.txt (to origin-target...) 
+ok                                
+move test2.txt (to origin-target...) 
+ok                                
+(recording state in git...)
++ git annex move --from origin-target
+move test1.txt (from origin-target...) 
+ok                                
+move test2.txt (from origin-target...) 
+ok                                
+(recording state in git...)
++ cd ..
+
+# 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/todo/remove_dependency_on_crypon_and_tls.mdwn b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
index d0ae5744f5..bea854ce1b 100644
--- a/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
+++ b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
@@ -26,10 +26,11 @@ Here is a plan:
 * Switch git-annex to botan for hash functions. There is already a build
   flag, but it would be easier if botan bundled the C library in the
   haskell package. Which they have considered doing.  
-  The `botan-default` branch makes the switch, but is blocked on needing
-  a way to install botan on windows. 
-  [cross compiling with MinGW](https://botan.randombit.net/handbook/building.html#cross-compiling)
-  may be needed, unless a prebuilt release can be found.
+  The `botan-default` branch makes the switch, but is blocked on
+  getting the windows build to work with botan. 
+  (I have wasted about 2 days on that so far, it seems
+  like a real toolchain nightmare. See gitannexbuilder github branch 
+  for the gory details.)
 * botan lacks skein and blake2, so would need to get those hashes another
   way
 * Switch from http-client-tls to http-client-openssl.

comment
diff --git a/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree/comment_1_f532e3ee98e21d37fe468a53d8ace300._comment b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree/comment_1_f532e3ee98e21d37fe468a53d8ace300._comment
new file mode 100644
index 0000000000..cb3cc3914d
--- /dev/null
+++ b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree/comment_1_f532e3ee98e21d37fe468a53d8ace300._comment
@@ -0,0 +1,43 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-29T20:50:56Z"
+ content="""
+This has nothing to do with `git-annex info`, any git-annex command after
+the first initialization behaves this way.
+
+It seems that git-annex is seeing the git config
+of "core.worktree=../../../../../../linked/sub" and getting a little bit
+confused. 
+
+It constructs a Repo with a location of eg
+
+	Local {gitdir = "/home/joey/tmp/tmp.rQ1fokJzh0/linked/sub/.git", worktree = Just "/home/linked/sub"}
+
+That worktree is wrong. And since it's not being run in that worktree,
+it tries to chdir to it.
+
+core.worktree is supposed to be relative to the gitdir. And
+since `.git -> ../../main/.git/worktrees/linked/modules/sub/`,
+that's "../../main/.git/worktrees/linked/modules/sub/../../../../../../linked/sub"
+aka "../../linked/sub".
+
+The wrong worktree comes from Git.Config.updateLocation. It is using the
+gitdir seen above, rather than following the symlink to the git directory it points to.
+So it uses "/home/joey/tmp/tmp.rQ1fokJzh0/linked/sub/.git/../../../../../../linked/sub"
+which reduces to "/home/linked/sub".
+
+So either Git.Config.updateLocation needs to change to follow the gitdir symlink,
+or the symlink needs to be followed at an earlier point.
+
+It's worth noting that the first time git-annex runs, it 
+ends up with the correct location eg
+
+	Local {gitdir = "/home/joey/tmp/tmp.D2wQYPhq9O/main/.git/worktrees/linked/modules/sub", worktree = Just "/home/joey/tmp/tmp.D2wQYPhq9O/linked/sub"}
+
+So the trouble comes from the various fixups that git-annex init has to do
+to convert .git files to symlinks.
+
+I will need to refresh my memory about linked worktrees and gitdir symlinks to get
+any further.
+"""]]

todo
diff --git a/doc/todo/p2phttp_--debug_--directory.mdwn b/doc/todo/p2phttp_--debug_--directory.mdwn
new file mode 100644
index 0000000000..915f2bea1c
--- /dev/null
+++ b/doc/todo/p2phttp_--debug_--directory.mdwn
@@ -0,0 +1,5 @@
+When using p2phttp --directory, --debug and other such flags have no effect.
+
+This should be possible to solve, by making `cmdnorepo` be passed
+an AnnexSetter. Then p2phttp could apply the settings of --debug and other
+such flags to each Annex. --[[Joey]]

todo
diff --git a/doc/todo/p2phttp_does_not_notice_newly_enabled_proxy.mdwn b/doc/todo/p2phttp_does_not_notice_newly_enabled_proxy.mdwn
new file mode 100644
index 0000000000..8563f906f1
--- /dev/null
+++ b/doc/todo/p2phttp_does_not_notice_newly_enabled_proxy.mdwn
@@ -0,0 +1,11 @@
+When a new proxy is configured in a repository, and p2phttp is serving that
+repository, it will not notice the new proxy, and so won't know how to
+serve its UUID, until it gets restarted.
+
+When serving a directory with multiple repositories, p2phttp is able to
+deal with a new repository being added. So maybe it also should be able to
+deal with a new proxy being configured? 
+
+This would entail making updateRepos scan for new proxies, basically.
+Then when a UUID that is not known is requested, it would rescan. Seems
+doable. --[[Joey]]

make ProtoAssociatedFile backwards compatible
This undoes b1cbcc7049ae7ebc68637455e2d23595ae801a17
-- make ProtoAssociatedFile round trip.
I hope one day to be able to revert this commit and get back to that
nice place. The comment explains when it might be possible to do so.
diff --git a/P2P/Protocol.hs b/P2P/Protocol.hs
index 0e3c298b5b..84d4426577 100644
--- a/P2P/Protocol.hs
+++ b/P2P/Protocol.hs
@@ -41,6 +41,7 @@ import System.Exit (ExitCode(..))
 import System.IO
 import qualified Data.ByteString.Lazy as L
 import qualified Data.Set as S
+import Data.Char
 import Data.Maybe
 import Data.Time.Clock.POSIX
 import Control.DeepSeq
@@ -228,20 +229,39 @@ instance Proto.Serializable Service where
 
 -- | Since ProtoAssociatedFile is not the last thing in a protocol line,
 -- its serialization cannot contain any whitespace. This is handled
--- by replacing whitespace with '%' (and '%' with '%%')
+-- by replacing ' ' with '%' (and '%' with '%%')
 --
 -- When deserializing an AssociatedFile from a peer, that escaping is
 -- reversed.
+--
+-- This does not currently round trip cleanly. That is done to avoid
+-- breaking backwards compatability with old versions of git-annex,
+-- where the P2P protocol split words on isSpace. Which is why this
+-- escapes '\t' and other spaces (including unicode spaces) to '%' as well.
+--
+-- Control characters are also sanitized. This sanitization is probably
+-- unncessary, since git-annex uses Utility.SafeOutput to avoid displaying
+-- control characters to the user.
+--
+-- Not roundtripping is ok because a ProtoAssociatedFile is normally
+-- only displayed to the user and so does not need to match a file on disk.
+-- (It may also be used in checking preferred content, which is very
+-- unlikely to care about spaces vs tabs or control characters.)
+--
+-- In the future, this could be made to round trip cleanly
+-- by only escaping ' ' and not sanitizing control characters.
+-- That should only be done once it's very unlikely that a peer
+-- will be running a version of git-annex older than 10.20261029.
 instance Proto.Serializable ProtoAssociatedFile where
 	serialize (ProtoAssociatedFile (AssociatedFile Nothing)) = ""
 	serialize (ProtoAssociatedFile (AssociatedFile (Just af))) = 
 		fromOsPath $ toInternalGitPath $
-			OS.concat $ map esc $ OS.unpack af
+			toOsPath $ concat $ map esc $ fromOsPath af
 	  where
-	  	esc c = case OS.toChar c of
-			'%' -> literalOsPath "%%"
-			' ' -> literalOsPath "%"
-			_ -> OS.singleton c
+	  	esc c
+			| c == '%' = "%%"
+			| isSpace c = "%"
+			| otherwise = [c]
 	
 	deserialize s = case fromInternalGitPath $ toOsPath $ deesc [] s of
 		f
@@ -254,7 +274,9 @@ instance Proto.Serializable ProtoAssociatedFile where
 	  	deesc b [] = reverse b
 		deesc b ('%':'%':cs) = deesc ('%':b) cs
 		deesc b ('%':cs) = deesc (' ':b) cs
-		deesc b (c:cs) = deesc (c:b) cs
+		deesc b (c:cs)
+			| isControl c = deesc ('_':b) cs
+			| otherwise = deesc (c:b) cs
 
 -- | Free monad for the protocol, combining net communication,
 -- and local actions.
diff --git a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace/comment_1_4128646029ba561b47a6d872e1eadeb5._comment b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace/comment_1_4128646029ba561b47a6d872e1eadeb5._comment
deleted file mode 100644
index c9a22b49a6..0000000000
--- a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace/comment_1_4128646029ba561b47a6d872e1eadeb5._comment
+++ /dev/null
@@ -1,8 +0,0 @@
-[[!comment format=mdwn
- username="joey"
- subject="""comment 1"""
- date="2026-10-29T15:37:53Z"
- content="""
-I have chosen to fix this on the server side. So the server will need to
-upgrade its git-annex-shell, and then this will work.
-"""]]

fix P2P protocol isSpace issues
Fix bug in parsing the P2P protocol that caused breakage when filenames
contained unicode multibyte space characters.
This bug was probably introduced as part of the RawFilePath (and then
OsPath) conversion, since before, the ProtoAssociatedFile instance
would have escaped all unicode isSpace characters and not only ascii spaces
as it does now.
But the real problem is that the P2P protocol has never been intended to
have the words separated by arbitrary unicode space, so splitting on
isSpace is just wrong. Splitting on ' ' fixes the problem.
Note that this means that, when talking to an old version of git-annex,
the multibyte space will be passed to it unchanged, and it then splits
on isSpace, so still fails.
It would be possible to interoperate with old versions, by going back to
escaping all unicode isSpace characters. I don't want to do that because
it moves the P2P protocol in a direction of complexity just to handle a
backwards compatability issue that is rare. I would rather move the P2P
protocol toward a simple correctness.
In situations where it fails, the remote git-annex is an old version,
that is buggy, and so needs to be upgraded.
diff --git a/CHANGELOG b/CHANGELOG
index 99ffd36bfa..fa47ee9724 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,8 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
   * git-annex.cabal: Switch from magic to libmagic.
   * Fix mimetype and mimencoding detection of non-ascii filenames 
     in non-unicode locales.
+  * Fix bug in parsing the P2P protocol that caused breakage when filenames
+    contained unicode multibyte space characters.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Utility/SimpleProtocol.hs b/Utility/SimpleProtocol.hs
index 73860216c2..ab921ed790 100644
--- a/Utility/SimpleProtocol.hs
+++ b/Utility/SimpleProtocol.hs
@@ -29,7 +29,6 @@ module Utility.SimpleProtocol (
 	getProtocolLine,
 ) where
 
-import Data.Char
 import GHC.IO.Handle
 import Text.Read
 
@@ -132,7 +131,7 @@ parse5 mk s = mk <$> deserialize p1 <*> deserialize p2 <*> deserialize p3 <*> de
 	(p4, p5) = splitWord rest''
 
 splitWord :: String -> (String, String)
-splitWord = separate isSpace
+splitWord = separate (== ' ')
 
 {- Only safe to use when the serialization does not include whitespace. -}
 parseList :: Serializable p => ([p] -> a) -> Parser a
diff --git a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
index a53e02f37a..e9880dafd7 100644
--- a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
+++ b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
@@ -43,3 +43,4 @@ Claude produced this patch, which I don't understand, but when running "git anne
 
 I've spend the last few days migrating *everything* to git annex, it's wonderful!
 
+> [[fixed|done]] --[[Joey]]
diff --git a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace/comment_1_4128646029ba561b47a6d872e1eadeb5._comment b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace/comment_1_4128646029ba561b47a6d872e1eadeb5._comment
new file mode 100644
index 0000000000..c9a22b49a6
--- /dev/null
+++ b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace/comment_1_4128646029ba561b47a6d872e1eadeb5._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-29T15:37:53Z"
+ content="""
+I have chosen to fix this on the server side. So the server will need to
+upgrade its git-annex-shell, and then this will work.
+"""]]

be more explict about how the space escaping works
diff --git a/doc/design/p2p_protocol.mdwn b/doc/design/p2p_protocol.mdwn
index d9cf75a02f..544859ad20 100644
--- a/doc/design/p2p_protocol.mdwn
+++ b/doc/design/p2p_protocol.mdwn
@@ -178,9 +178,9 @@ Here AssociatedFile may be the name of a file in the git
 repository, for information purposes only. Or it can be the
 empty string. It will always have unix directory separators.
 
-(Note that in the line-based serialization. AssociatedFile may not contain any
-spaces, since it's not the last token in the line. Use '%' to indicate
-whitespace.)
+(Note that in the line-based serialization. AssociatedFile may not contain
+any spaces, since it's not the last token in the line. Space is escaped
+to '%' and '%' is escaped to '%%'.)
 
 The server may respond with ALREADY-HAVE if it already
 had the content of that key. 

fix tag
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index 9024ef8553..9ffa5fb495 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -50,4 +50,4 @@ that any fix to the main attack will also close off this one.)
 
 --[[Joey]]
 
-[fixed|done]]
+[[fixed|done]]

diff --git a/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn
new file mode 100644
index 0000000000..866d9d9088
--- /dev/null
+++ b/doc/bugs/info_fails_to_resolve_from_submodule_in_worktree.mdwn
@@ -0,0 +1,64 @@
+### Please describe the problem in your own words.
+
+Currently the command `git annex info` fails   when run inside a submodule that is checked out in a linked Git worktree. The error it provides is:
+
+`changeWorkingDirectory: does not exist `
+
+Ordinary Git commands recognize the worktree correctly, and the same annex command succeeds when `GIT_DIR` and `GIT_WORK_TREE` are explicitly set to absolute paths. 
+
+I think it should work without those overrides.
+
+### What steps will reproduce the problem?
+
+
+This bash script should reproduce the problem on most systems:
+
+```
+#!/bin/sh
+set -eu
+
+workdir=$(mktemp -d)
+cd "$workdir"
+workdir=$(pwd -P)
+
+# Create a repository to use as a submodule.
+git init source
+git -C source commit --allow-empty -m "Initial commit"
+
+# Add it to a parent repository.
+git init main
+git -C main -c protocol.file.allow=always submodule add "$workdir/source" sub
+git -C main commit -m "Add submodule"
+
+# Check out the submodule in a linked worktree.
+git -C main worktree add -b linked ../linked
+git -C linked -c protocol.file.allow=always submodule update --init
+cd linked/sub
+git annex init repro
+
+# Ordinary Git recognizes the worktree.
+git rev-parse --show-toplevel
+git status
+
+# This fails with "changeWorkingDirectory: does not exist".
+# Continue so we can also demonstrate the workaround.
+git annex info || true
+
+# Explicit absolute paths make the same command succeed.
+GIT_DIR=$(git rev-parse --absolute-git-dir) \
+GIT_WORK_TREE=$(pwd -P) \
+git annex info
+```
+
+### What version of git-annex are you using? On what operating system?
+
+I have tried it across a range of versions.
+
+### Please provide any additional information below.
+
+[[!tag projects/repronim]]
+
+### Have you had any luck using git-annex before?
+
+Yes, lots. It's been working well for me in keeping track of scientific analyses.
+

update
diff --git a/doc/todo/remove_dependency_on_crypon_and_tls.mdwn b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
index 4b2a04097f..d0ae5744f5 100644
--- a/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
+++ b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
@@ -25,7 +25,11 @@ Here is a plan:
 
 * Switch git-annex to botan for hash functions. There is already a build
   flag, but it would be easier if botan bundled the C library in the
-  haskell package. Which they have considered doing.
+  haskell package. Which they have considered doing.  
+  The `botan-default` branch makes the switch, but is blocked on needing
+  a way to install botan on windows. 
+  [cross compiling with MinGW](https://botan.randombit.net/handbook/building.html#cross-compiling)
+  may be needed, unless a prebuilt release can be found.
 * botan lacks skein and blake2, so would need to get those hashes another
   way
 * Switch from http-client-tls to http-client-openssl.

update
diff --git a/doc/todo/remove_dependency_on_crypon_and_tls.mdwn b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
index a2982187b6..4b2a04097f 100644
--- a/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
+++ b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
@@ -33,5 +33,12 @@ Here is a plan:
   the webapp and p2phttp; the webapp is mostly used on localhost and
   p2phttp is often used behind a server (eg forgejo) that can handle the
   https if it only supported http.
-* Make aws somehow able to use http-client-openssl (eg, by parameterizing it)
-* Make DAV somehow able to use http-client-openssl (eg, by parameterizing it)
+* aws depends on http-client-tls, but only uses it in its test suite;
+  the library uses the Manager that the user passes in.
+  So it would be possible to depend on aws w/o getting http-client-tls as a
+  dependency, except it has a gratuitous dependency on it in the 
+  Library stanza of the cabal file. Easily fixed.
+* Avoid DAV depending on http-client-tls, by passing a Manager to
+  mkDAVContext. (The context can be updated after the fact to use a
+  http-client-tls manager, but it still uses http-client-tls first to make
+  one.)

close
diff --git a/doc/todo/libmagic.mdwn b/doc/todo/libmagic.mdwn
index 8d59dc4fc7..5a61f41274 100644
--- a/doc/todo/libmagic.mdwn
+++ b/doc/todo/libmagic.mdwn
@@ -1,14 +1,3 @@
 The `libmagic` branch switches to a fork of the magic library.
 
-Before merging it, need a test case that filenames travel end to end
-through git-annex to C libmagic correctly. Particularly on Windows
-with filenames that end up containing wide characters.
-There is some possibility of new problems, due to the change to OsPath.
-Seems low, but not tested yet. --[[Joey]]
-
-> Update: Added the test case, waiting to see if it works on windows with
-> magic before switching over. --[[Joey]]
-
-> > Update: The test case fails on windows with magic. Merged libmagic,
-> > will see if that fixes it on windows. In any case, libmagic can't make 
-> > the situation worse. --[[Joey]]
+> [[done]] --[[Joey]]

libmagic 1.3.1 working
Except non-ascii on windows, which has never worked. So disable the
failing test case on windows for now.
Interesting that magicFD didn't work on windows at all. It would be
worth investigating to see how it fails.
diff --git a/CHANGELOG b/CHANGELOG
index cc2616c4bd..99ffd36bfa 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,8 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
     and will be removed after this release. Android users are recommended
     to switch to running `pkg install git-annex` inside termux.
   * git-annex.cabal: Switch from magic to libmagic.
+  * Fix mimetype and mimencoding detection of non-ascii filenames 
+    in non-unicode locales.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Test.hs b/Test.hs
index 32d020e832..7d846cd712 100644
--- a/Test.hs
+++ b/Test.hs
@@ -577,8 +577,12 @@ test_magic = intmpclonerepo $ do
 		@? "binary file not added to annex despite mimeencoding config"
 	(not <$> addedannex (literalOsPath "text"))
 		@? "non-binary file got added to annex despite mimeencoding config"
+#ifndef mingw32_HOST_OS
+	-- This is a known failure on windows, see 
+	-- doc/bugs/mimeencoding_detection_is_not_working.mdwn
 	addedannex unicodefn
 		@? "binary file with unicode name not added to annex despite mimeencoding config"
+#endif
   where
 	binarycontent = "\127"
 	-- quasiquoted because fromString for OsString mangles unicode
diff --git a/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment b/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
index db5e0b40f3..7d9b491eae 100644
--- a/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
+++ b/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
@@ -14,14 +14,5 @@ package, which has some very dodgy LLM generated code around filesystem
 encodings. I have not tested it.)
 
 git-annex has now switched to the haskell libmagic package. That fixes
-the problem on linux with LANG=C. 
-
-The problem still exists on windows though. I think it needs fixing in
-libmagic probably. Also libmagic caused a reversion, even plain ascii
-filenames now don't get handled right on windows. The test suite catches
-that too:
-
-	 magic:                              FAIL (10.51s)
-	      Test.hs:577:
-	      binary file not added to annex despite mimeencoding config
+the problem on linux with LANG=C.
 """]]
diff --git a/doc/bugs/mimeencoding_detection_is_not_working/comment_5_fb0e009ebcde23bdbd8055d94ca49aba._comment b/doc/bugs/mimeencoding_detection_is_not_working/comment_5_fb0e009ebcde23bdbd8055d94ca49aba._comment
new file mode 100644
index 0000000000..c053de907a
--- /dev/null
+++ b/doc/bugs/mimeencoding_detection_is_not_working/comment_5_fb0e009ebcde23bdbd8055d94ca49aba._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2026-10-28T01:39:46Z"
+ content="""
+The problem still exists on windows though. I think it may not be
+fixable without modifying the C libmagic to use `_wfopen()` or something
+like that. It may not be able to open non-ascii files at all currently.
+
+I tried passing a FD to get around the problem, 
+but that also didn't work right on windows.
+
+For now I've disabled the failing test on windows.
+"""]]

try libmagic 1.3.1 w/o magicFD
Possibly magicFD doesn't work on windows for whatever reason,
but 1.3.1's fixes will make it otherwise work except for the
old problem with unicode filenames.
diff --git a/Annex/Magic.hs b/Annex/Magic.hs
index 4771adada4..b8fb2d02b7 100644
--- a/Annex/Magic.hs
+++ b/Annex/Magic.hs
@@ -1,6 +1,6 @@
 {- Interface to libmagic
  -
- - Copyright 2019-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -13,8 +13,10 @@ module Annex.Magic (
 	MimeType,
 	MimeEncoding,
 	initMagicMime,
+	getMagicMime,
 	getMagicMimeType,
 	getMagicMimeEncoding,
+	parseMagicMime,
 ) where
 
 import Common
@@ -23,8 +25,9 @@ import Control.Monad.IO.Class
 #ifdef WITH_MAGICMIME
 import Magic
 import Utility.Env
-import Control.Concurrent
-import System.IO.Unsafe (unsafePerformIO)
+import Data.Char
+import qualified Data.List.NonEmpty as NE
+import qualified Data.ByteString as B
 #else
 type Magic = ()
 #endif
@@ -35,7 +38,7 @@ initMagicMime = catchMaybeIO $ do
 	m <- magicOpen [MagicMime]
 	liftIO $ getEnv "GIT_ANNEX_DIR" >>= \case
 		Nothing -> magicLoadDefault m
-		Just d -> magicLoad m $ fromOsPath $
+		Just d -> magicLoad m $ NE.singleton $
 			toOsPath d
 				</> literalOsPath "magic"
 				</> literalOsPath "magic.mgc"
@@ -46,32 +49,20 @@ initMagicMime = return Nothing
 
 getMagicMime :: Magic -> OsPath -> IO (Maybe (MimeType, MimeEncoding))
 #ifdef WITH_MAGICMIME
-getMagicMime m f = Just . parse <$> magicConcurrentSafe (magicFile m (fromOsPath f))
-  where
-	parse s = 
-		let (mimetype, rest) = separate (== ';') s
-		in case rest of
-			(' ':'c':'h':'a':'r':'s':'e':'t':'=':mimeencoding) -> 
-				(mimetype, mimeencoding)
-			_ -> (mimetype, "")
+getMagicMime m f = Just . parseMagicMime <$> magicFile m f
 #else
 getMagicMime _ _ = return Nothing
 #endif
 
+parseMagicMime :: B.ByteString -> (MimeType, MimeEncoding)
+parseMagicMime b = (decodeBS mimetype, decodeBS mimeencoding)
+  where
+	(mimetype, rest) = B.break (== semicolon) b
+	mimeencoding = fromMaybe mempty (B.stripPrefix "; charset=" rest)
+	semicolon = fromIntegral (ord ';')
+
 getMagicMimeType :: MonadIO m => Magic -> OsPath -> m (Maybe MimeType)
 getMagicMimeType m f = liftIO $ fmap fst <$> getMagicMime m f
 
 getMagicMimeEncoding :: MonadIO m => Magic -> OsPath -> m(Maybe MimeEncoding)
 getMagicMimeEncoding m f = liftIO $ fmap snd <$> getMagicMime m f
-
-#ifdef WITH_MAGICMIME
-{-# NOINLINE mutex #-}
-mutex :: MVar ()
-mutex = unsafePerformIO $ newMVar ()
-
--- Work around a bug, the library is not concurrency safe and will
--- sometimes access the wrong memory if multiple ones are called at the
--- same time.
-magicConcurrentSafe :: IO a -> IO a
-magicConcurrentSafe = bracket_ (takeMVar mutex) (putMVar mutex ())
-#endif
diff --git a/CHANGELOG b/CHANGELOG
index 294306b26c..cc2616c4bd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,7 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
   * The Linux arm64-ancient build has reached end of security support 
     and will be removed after this release. Android users are recommended
     to switch to running `pkg install git-annex` inside termux.
+  * git-annex.cabal: Switch from magic to libmagic.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Test.hs b/Test.hs
index e069f6c4a6..32d020e832 100644
--- a/Test.hs
+++ b/Test.hs
@@ -1,11 +1,12 @@
 {- git-annex test suite
  -
- - Copyright 2010-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE CPP #-}
 
 module Test where
@@ -27,6 +28,7 @@ import Control.Concurrent.STM hiding (check)
 import qualified Utility.RawFilePath as R
 import qualified Data.List.NonEmpty as NE
 import Data.String
+import System.OsString (osstr)
 
 import Common
 import CmdLine.GitAnnex.Options
@@ -564,16 +566,24 @@ test_magic = intmpclonerepo $ do
 #ifdef WITH_MAGICMIME
 	git "config" ["annex.largefiles", "mimeencoding=binary"]
 		"git config annex.largefiles"
-	writeFileString (literalOsPath "binary") "\127"
+	writeFileString (literalOsPath "binary") binarycontent
 	writeFileString (literalOsPath "text") "test\n" 
-	git_annex "add" ["binary", "text"]
+	writeFileString unicodefn binarycontent
+	git_annex "add" []
 		"git-annex add with mimeencoding in largefiles"
 	git_annex "sync" ["--no-content"]
 		"git-annex sync"
-	(isJust <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "binary")))
+	addedannex (literalOsPath "binary")
 		@? "binary file not added to annex despite mimeencoding config"
-	(isNothing <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "text")))
+	(not <$> addedannex (literalOsPath "text"))
 		@? "non-binary file got added to annex despite mimeencoding config"
+	addedannex unicodefn
+		@? "binary file with unicode name not added to annex despite mimeencoding config"
+  where
+	binarycontent = "\127"
+	-- quasiquoted because fromString for OsString mangles unicode
+	unicodefn = [osstr|fo¡oüঊ|]
+	addedannex f = isJust <$> annexeval (Annex.CatFile.catKeyFile f)
 #else
 	return ()
 #endif
diff --git a/debian/control b/debian/control
index 3c80b61f48..93368f0656 100644
--- a/debian/control
+++ b/debian/control
@@ -77,7 +77,7 @@ Build-Depends:
 	libghc-unbounded-delays-dev,
 	libghc-disk-free-space-dev,
 	libghc-mountpoints-dev,
-	libghc-magic-dev,
+	libghc-libmagic-dev,
 	libghc-socks-dev,
 	libghc-vector-dev,
 	libghc-unliftio-core-dev,
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 0f1e7361f1..35a415330e 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -100,17 +100,6 @@ and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
 
-### magic
-
-[magic](https://hackage.haskell.org/package/magic) since 1.1.1
-
-[First disclosed LLM generated code](https://github.com/philippedev101/magic-haskell/commit/f52a3628626644d68e1cd1d2c36e98f78693bda5)
-
-The person who took over magic from its long-time maintainer has not
-disclosed their earlier LLM use, but it is apparent, including in their
-communications to [[Joey]] about incorrect LLM-generated statements in the
-changelog.
-
 ### crypton
 
 [crypton](https://hackage.haskell.org/package/crypton) since 1.1.0
@@ -174,3 +163,22 @@ is a way that makes them not be a potential license problem.
 which was merged into gitk upstream of git and so avoided git's policy.
 
 git-annex supports git back to 2.22.
+
+## previous dependencies that contained LLM generated code
+
+[[!toc startlevel=3]]
+
+### magic
+
+[magic](https://hackage.haskell.org/package/magic) since 1.1.1
+

(Diff truncated)
revert libmagic again
This reverts commit c51f910bc4d7d4be682fc7cfefb715b885e2f1a1.
Still failing on windows:
binary file not added to annex despite mimeencoding config
diff --git a/Annex/Magic.hs b/Annex/Magic.hs
index b8fb2d02b7..4771adada4 100644
--- a/Annex/Magic.hs
+++ b/Annex/Magic.hs
@@ -1,6 +1,6 @@
 {- Interface to libmagic
  -
- - Copyright 2019-2026 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -13,10 +13,8 @@ module Annex.Magic (
 	MimeType,
 	MimeEncoding,
 	initMagicMime,
-	getMagicMime,
 	getMagicMimeType,
 	getMagicMimeEncoding,
-	parseMagicMime,
 ) where
 
 import Common
@@ -25,9 +23,8 @@ import Control.Monad.IO.Class
 #ifdef WITH_MAGICMIME
 import Magic
 import Utility.Env
-import Data.Char
-import qualified Data.List.NonEmpty as NE
-import qualified Data.ByteString as B
+import Control.Concurrent
+import System.IO.Unsafe (unsafePerformIO)
 #else
 type Magic = ()
 #endif
@@ -38,7 +35,7 @@ initMagicMime = catchMaybeIO $ do
 	m <- magicOpen [MagicMime]
 	liftIO $ getEnv "GIT_ANNEX_DIR" >>= \case
 		Nothing -> magicLoadDefault m
-		Just d -> magicLoad m $ NE.singleton $
+		Just d -> magicLoad m $ fromOsPath $
 			toOsPath d
 				</> literalOsPath "magic"
 				</> literalOsPath "magic.mgc"
@@ -49,20 +46,32 @@ initMagicMime = return Nothing
 
 getMagicMime :: Magic -> OsPath -> IO (Maybe (MimeType, MimeEncoding))
 #ifdef WITH_MAGICMIME
-getMagicMime m f = Just . parseMagicMime <$> magicFile m f
+getMagicMime m f = Just . parse <$> magicConcurrentSafe (magicFile m (fromOsPath f))
+  where
+	parse s = 
+		let (mimetype, rest) = separate (== ';') s
+		in case rest of
+			(' ':'c':'h':'a':'r':'s':'e':'t':'=':mimeencoding) -> 
+				(mimetype, mimeencoding)
+			_ -> (mimetype, "")
 #else
 getMagicMime _ _ = return Nothing
 #endif
 
-parseMagicMime :: B.ByteString -> (MimeType, MimeEncoding)
-parseMagicMime b = (decodeBS mimetype, decodeBS mimeencoding)
-  where
-	(mimetype, rest) = B.break (== semicolon) b
-	mimeencoding = fromMaybe mempty (B.stripPrefix "; charset=" rest)
-	semicolon = fromIntegral (ord ';')
-
 getMagicMimeType :: MonadIO m => Magic -> OsPath -> m (Maybe MimeType)
 getMagicMimeType m f = liftIO $ fmap fst <$> getMagicMime m f
 
 getMagicMimeEncoding :: MonadIO m => Magic -> OsPath -> m(Maybe MimeEncoding)
 getMagicMimeEncoding m f = liftIO $ fmap snd <$> getMagicMime m f
+
+#ifdef WITH_MAGICMIME
+{-# NOINLINE mutex #-}
+mutex :: MVar ()
+mutex = unsafePerformIO $ newMVar ()
+
+-- Work around a bug, the library is not concurrency safe and will
+-- sometimes access the wrong memory if multiple ones are called at the
+-- same time.
+magicConcurrentSafe :: IO a -> IO a
+magicConcurrentSafe = bracket_ (takeMVar mutex) (putMVar mutex ())
+#endif
diff --git a/CHANGELOG b/CHANGELOG
index cc2616c4bd..294306b26c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,7 +3,6 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
   * The Linux arm64-ancient build has reached end of security support 
     and will be removed after this release. Android users are recommended
     to switch to running `pkg install git-annex` inside termux.
-  * git-annex.cabal: Switch from magic to libmagic.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Test.hs b/Test.hs
index 32d020e832..e069f6c4a6 100644
--- a/Test.hs
+++ b/Test.hs
@@ -1,12 +1,11 @@
 {- git-annex test suite
  -
- - Copyright 2010-2026 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE CPP #-}
 
 module Test where
@@ -28,7 +27,6 @@ import Control.Concurrent.STM hiding (check)
 import qualified Utility.RawFilePath as R
 import qualified Data.List.NonEmpty as NE
 import Data.String
-import System.OsString (osstr)
 
 import Common
 import CmdLine.GitAnnex.Options
@@ -566,24 +564,16 @@ test_magic = intmpclonerepo $ do
 #ifdef WITH_MAGICMIME
 	git "config" ["annex.largefiles", "mimeencoding=binary"]
 		"git config annex.largefiles"
-	writeFileString (literalOsPath "binary") binarycontent
+	writeFileString (literalOsPath "binary") "\127"
 	writeFileString (literalOsPath "text") "test\n" 
-	writeFileString unicodefn binarycontent
-	git_annex "add" []
+	git_annex "add" ["binary", "text"]
 		"git-annex add with mimeencoding in largefiles"
 	git_annex "sync" ["--no-content"]
 		"git-annex sync"
-	addedannex (literalOsPath "binary")
+	(isJust <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "binary")))
 		@? "binary file not added to annex despite mimeencoding config"
-	(not <$> addedannex (literalOsPath "text"))
+	(isNothing <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "text")))
 		@? "non-binary file got added to annex despite mimeencoding config"
-	addedannex unicodefn
-		@? "binary file with unicode name not added to annex despite mimeencoding config"
-  where
-	binarycontent = "\127"
-	-- quasiquoted because fromString for OsString mangles unicode
-	unicodefn = [osstr|fo¡oüঊ|]
-	addedannex f = isJust <$> annexeval (Annex.CatFile.catKeyFile f)
 #else
 	return ()
 #endif
diff --git a/debian/control b/debian/control
index 93368f0656..3c80b61f48 100644
--- a/debian/control
+++ b/debian/control
@@ -77,7 +77,7 @@ Build-Depends:
 	libghc-unbounded-delays-dev,
 	libghc-disk-free-space-dev,
 	libghc-mountpoints-dev,
-	libghc-libmagic-dev,
+	libghc-magic-dev,
 	libghc-socks-dev,
 	libghc-vector-dev,
 	libghc-unliftio-core-dev,
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 35a415330e..0f1e7361f1 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -100,6 +100,17 @@ and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
 
+### magic
+
+[magic](https://hackage.haskell.org/package/magic) since 1.1.1
+
+[First disclosed LLM generated code](https://github.com/philippedev101/magic-haskell/commit/f52a3628626644d68e1cd1d2c36e98f78693bda5)
+
+The person who took over magic from its long-time maintainer has not
+disclosed their earlier LLM use, but it is apparent, including in their
+communications to [[Joey]] about incorrect LLM-generated statements in the
+changelog.
+
 ### crypton
 
 [crypton](https://hackage.haskell.org/package/crypton) since 1.1.0
@@ -163,22 +174,3 @@ is a way that makes them not be a potential license problem.
 which was merged into gitk upstream of git and so avoided git's policy.
 
 git-annex supports git back to 2.22.
-
-## previous dependencies that contained LLM generated code
-
-[[!toc startlevel=3]]
-
-### magic
-
-[magic](https://hackage.haskell.org/package/magic) since 1.1.1
-

(Diff truncated)
Reapply "git-annex.cabal: Switch from magic to libmagic"
This reverts commit e947d3a6f74a24d91d9fc6d3610e78e34a5f7e40.
diff --git a/Annex/Magic.hs b/Annex/Magic.hs
index 4771adada4..b8fb2d02b7 100644
--- a/Annex/Magic.hs
+++ b/Annex/Magic.hs
@@ -1,6 +1,6 @@
 {- Interface to libmagic
  -
- - Copyright 2019-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -13,8 +13,10 @@ module Annex.Magic (
 	MimeType,
 	MimeEncoding,
 	initMagicMime,
+	getMagicMime,
 	getMagicMimeType,
 	getMagicMimeEncoding,
+	parseMagicMime,
 ) where
 
 import Common
@@ -23,8 +25,9 @@ import Control.Monad.IO.Class
 #ifdef WITH_MAGICMIME
 import Magic
 import Utility.Env
-import Control.Concurrent
-import System.IO.Unsafe (unsafePerformIO)
+import Data.Char
+import qualified Data.List.NonEmpty as NE
+import qualified Data.ByteString as B
 #else
 type Magic = ()
 #endif
@@ -35,7 +38,7 @@ initMagicMime = catchMaybeIO $ do
 	m <- magicOpen [MagicMime]
 	liftIO $ getEnv "GIT_ANNEX_DIR" >>= \case
 		Nothing -> magicLoadDefault m
-		Just d -> magicLoad m $ fromOsPath $
+		Just d -> magicLoad m $ NE.singleton $
 			toOsPath d
 				</> literalOsPath "magic"
 				</> literalOsPath "magic.mgc"
@@ -46,32 +49,20 @@ initMagicMime = return Nothing
 
 getMagicMime :: Magic -> OsPath -> IO (Maybe (MimeType, MimeEncoding))
 #ifdef WITH_MAGICMIME
-getMagicMime m f = Just . parse <$> magicConcurrentSafe (magicFile m (fromOsPath f))
-  where
-	parse s = 
-		let (mimetype, rest) = separate (== ';') s
-		in case rest of
-			(' ':'c':'h':'a':'r':'s':'e':'t':'=':mimeencoding) -> 
-				(mimetype, mimeencoding)
-			_ -> (mimetype, "")
+getMagicMime m f = Just . parseMagicMime <$> magicFile m f
 #else
 getMagicMime _ _ = return Nothing
 #endif
 
+parseMagicMime :: B.ByteString -> (MimeType, MimeEncoding)
+parseMagicMime b = (decodeBS mimetype, decodeBS mimeencoding)
+  where
+	(mimetype, rest) = B.break (== semicolon) b
+	mimeencoding = fromMaybe mempty (B.stripPrefix "; charset=" rest)
+	semicolon = fromIntegral (ord ';')
+
 getMagicMimeType :: MonadIO m => Magic -> OsPath -> m (Maybe MimeType)
 getMagicMimeType m f = liftIO $ fmap fst <$> getMagicMime m f
 
 getMagicMimeEncoding :: MonadIO m => Magic -> OsPath -> m(Maybe MimeEncoding)
 getMagicMimeEncoding m f = liftIO $ fmap snd <$> getMagicMime m f
-
-#ifdef WITH_MAGICMIME
-{-# NOINLINE mutex #-}
-mutex :: MVar ()
-mutex = unsafePerformIO $ newMVar ()
-
--- Work around a bug, the library is not concurrency safe and will
--- sometimes access the wrong memory if multiple ones are called at the
--- same time.
-magicConcurrentSafe :: IO a -> IO a
-magicConcurrentSafe = bracket_ (takeMVar mutex) (putMVar mutex ())
-#endif
diff --git a/CHANGELOG b/CHANGELOG
index 294306b26c..cc2616c4bd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,7 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
   * The Linux arm64-ancient build has reached end of security support 
     and will be removed after this release. Android users are recommended
     to switch to running `pkg install git-annex` inside termux.
+  * git-annex.cabal: Switch from magic to libmagic.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Test.hs b/Test.hs
index e069f6c4a6..32d020e832 100644
--- a/Test.hs
+++ b/Test.hs
@@ -1,11 +1,12 @@
 {- git-annex test suite
  -
- - Copyright 2010-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE CPP #-}
 
 module Test where
@@ -27,6 +28,7 @@ import Control.Concurrent.STM hiding (check)
 import qualified Utility.RawFilePath as R
 import qualified Data.List.NonEmpty as NE
 import Data.String
+import System.OsString (osstr)
 
 import Common
 import CmdLine.GitAnnex.Options
@@ -564,16 +566,24 @@ test_magic = intmpclonerepo $ do
 #ifdef WITH_MAGICMIME
 	git "config" ["annex.largefiles", "mimeencoding=binary"]
 		"git config annex.largefiles"
-	writeFileString (literalOsPath "binary") "\127"
+	writeFileString (literalOsPath "binary") binarycontent
 	writeFileString (literalOsPath "text") "test\n" 
-	git_annex "add" ["binary", "text"]
+	writeFileString unicodefn binarycontent
+	git_annex "add" []
 		"git-annex add with mimeencoding in largefiles"
 	git_annex "sync" ["--no-content"]
 		"git-annex sync"
-	(isJust <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "binary")))
+	addedannex (literalOsPath "binary")
 		@? "binary file not added to annex despite mimeencoding config"
-	(isNothing <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "text")))
+	(not <$> addedannex (literalOsPath "text"))
 		@? "non-binary file got added to annex despite mimeencoding config"
+	addedannex unicodefn
+		@? "binary file with unicode name not added to annex despite mimeencoding config"
+  where
+	binarycontent = "\127"
+	-- quasiquoted because fromString for OsString mangles unicode
+	unicodefn = [osstr|fo¡oüঊ|]
+	addedannex f = isJust <$> annexeval (Annex.CatFile.catKeyFile f)
 #else
 	return ()
 #endif
diff --git a/debian/control b/debian/control
index 3c80b61f48..93368f0656 100644
--- a/debian/control
+++ b/debian/control
@@ -77,7 +77,7 @@ Build-Depends:
 	libghc-unbounded-delays-dev,
 	libghc-disk-free-space-dev,
 	libghc-mountpoints-dev,
-	libghc-magic-dev,
+	libghc-libmagic-dev,
 	libghc-socks-dev,
 	libghc-vector-dev,
 	libghc-unliftio-core-dev,
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 0f1e7361f1..35a415330e 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -100,17 +100,6 @@ and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
 
-### magic
-
-[magic](https://hackage.haskell.org/package/magic) since 1.1.1
-
-[First disclosed LLM generated code](https://github.com/philippedev101/magic-haskell/commit/f52a3628626644d68e1cd1d2c36e98f78693bda5)
-
-The person who took over magic from its long-time maintainer has not
-disclosed their earlier LLM use, but it is apparent, including in their
-communications to [[Joey]] about incorrect LLM-generated statements in the
-changelog.
-
 ### crypton
 
 [crypton](https://hackage.haskell.org/package/crypton) since 1.1.0
@@ -174,3 +163,22 @@ is a way that makes them not be a potential license problem.
 which was merged into gitk upstream of git and so avoided git's policy.
 
 git-annex supports git back to 2.22.
+
+## previous dependencies that contained LLM generated code
+
+[[!toc startlevel=3]]
+
+### magic
+
+[magic](https://hackage.haskell.org/package/magic) since 1.1.1
+

(Diff truncated)
Revert "git-annex.cabal: Switch from magic to libmagic"
This reverts commit bc5cf5549635d0b9b83bb21032a1a1a02c1ce424 and
following.
This is a temporary revert until a reversion on windows can be fixed.
diff --git a/Annex/Magic.hs b/Annex/Magic.hs
index b8fb2d02b7..4771adada4 100644
--- a/Annex/Magic.hs
+++ b/Annex/Magic.hs
@@ -1,6 +1,6 @@
 {- Interface to libmagic
  -
- - Copyright 2019-2026 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2020 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -13,10 +13,8 @@ module Annex.Magic (
 	MimeType,
 	MimeEncoding,
 	initMagicMime,
-	getMagicMime,
 	getMagicMimeType,
 	getMagicMimeEncoding,
-	parseMagicMime,
 ) where
 
 import Common
@@ -25,9 +23,8 @@ import Control.Monad.IO.Class
 #ifdef WITH_MAGICMIME
 import Magic
 import Utility.Env
-import Data.Char
-import qualified Data.List.NonEmpty as NE
-import qualified Data.ByteString as B
+import Control.Concurrent
+import System.IO.Unsafe (unsafePerformIO)
 #else
 type Magic = ()
 #endif
@@ -38,7 +35,7 @@ initMagicMime = catchMaybeIO $ do
 	m <- magicOpen [MagicMime]
 	liftIO $ getEnv "GIT_ANNEX_DIR" >>= \case
 		Nothing -> magicLoadDefault m
-		Just d -> magicLoad m $ NE.singleton $
+		Just d -> magicLoad m $ fromOsPath $
 			toOsPath d
 				</> literalOsPath "magic"
 				</> literalOsPath "magic.mgc"
@@ -49,20 +46,32 @@ initMagicMime = return Nothing
 
 getMagicMime :: Magic -> OsPath -> IO (Maybe (MimeType, MimeEncoding))
 #ifdef WITH_MAGICMIME
-getMagicMime m f = Just . parseMagicMime <$> magicFile m f
+getMagicMime m f = Just . parse <$> magicConcurrentSafe (magicFile m (fromOsPath f))
+  where
+	parse s = 
+		let (mimetype, rest) = separate (== ';') s
+		in case rest of
+			(' ':'c':'h':'a':'r':'s':'e':'t':'=':mimeencoding) -> 
+				(mimetype, mimeencoding)
+			_ -> (mimetype, "")
 #else
 getMagicMime _ _ = return Nothing
 #endif
 
-parseMagicMime :: B.ByteString -> (MimeType, MimeEncoding)
-parseMagicMime b = (decodeBS mimetype, decodeBS mimeencoding)
-  where
-	(mimetype, rest) = B.break (== semicolon) b
-	mimeencoding = fromMaybe mempty (B.stripPrefix "; charset=" rest)
-	semicolon = fromIntegral (ord ';')
-
 getMagicMimeType :: MonadIO m => Magic -> OsPath -> m (Maybe MimeType)
 getMagicMimeType m f = liftIO $ fmap fst <$> getMagicMime m f
 
 getMagicMimeEncoding :: MonadIO m => Magic -> OsPath -> m(Maybe MimeEncoding)
 getMagicMimeEncoding m f = liftIO $ fmap snd <$> getMagicMime m f
+
+#ifdef WITH_MAGICMIME
+{-# NOINLINE mutex #-}
+mutex :: MVar ()
+mutex = unsafePerformIO $ newMVar ()
+
+-- Work around a bug, the library is not concurrency safe and will
+-- sometimes access the wrong memory if multiple ones are called at the
+-- same time.
+magicConcurrentSafe :: IO a -> IO a
+magicConcurrentSafe = bracket_ (takeMVar mutex) (putMVar mutex ())
+#endif
diff --git a/CHANGELOG b/CHANGELOG
index cc2616c4bd..294306b26c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,7 +3,6 @@ git-annex (10.20261027) UNRELEASED; urgency=medium
   * The Linux arm64-ancient build has reached end of security support 
     and will be removed after this release. Android users are recommended
     to switch to running `pkg install git-annex` inside termux.
-  * git-annex.cabal: Switch from magic to libmagic.
 
  -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
 
diff --git a/Test.hs b/Test.hs
index 32d020e832..e069f6c4a6 100644
--- a/Test.hs
+++ b/Test.hs
@@ -1,12 +1,11 @@
 {- git-annex test suite
  -
- - Copyright 2010-2026 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE CPP #-}
 
 module Test where
@@ -28,7 +27,6 @@ import Control.Concurrent.STM hiding (check)
 import qualified Utility.RawFilePath as R
 import qualified Data.List.NonEmpty as NE
 import Data.String
-import System.OsString (osstr)
 
 import Common
 import CmdLine.GitAnnex.Options
@@ -566,24 +564,16 @@ test_magic = intmpclonerepo $ do
 #ifdef WITH_MAGICMIME
 	git "config" ["annex.largefiles", "mimeencoding=binary"]
 		"git config annex.largefiles"
-	writeFileString (literalOsPath "binary") binarycontent
+	writeFileString (literalOsPath "binary") "\127"
 	writeFileString (literalOsPath "text") "test\n" 
-	writeFileString unicodefn binarycontent
-	git_annex "add" []
+	git_annex "add" ["binary", "text"]
 		"git-annex add with mimeencoding in largefiles"
 	git_annex "sync" ["--no-content"]
 		"git-annex sync"
-	addedannex (literalOsPath "binary")
+	(isJust <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "binary")))
 		@? "binary file not added to annex despite mimeencoding config"
-	(not <$> addedannex (literalOsPath "text"))
+	(isNothing <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "text")))
 		@? "non-binary file got added to annex despite mimeencoding config"
-	addedannex unicodefn
-		@? "binary file with unicode name not added to annex despite mimeencoding config"
-  where
-	binarycontent = "\127"
-	-- quasiquoted because fromString for OsString mangles unicode
-	unicodefn = [osstr|fo¡oüঊ|]
-	addedannex f = isJust <$> annexeval (Annex.CatFile.catKeyFile f)
 #else
 	return ()
 #endif
diff --git a/debian/control b/debian/control
index 93368f0656..3c80b61f48 100644
--- a/debian/control
+++ b/debian/control
@@ -77,7 +77,7 @@ Build-Depends:
 	libghc-unbounded-delays-dev,
 	libghc-disk-free-space-dev,
 	libghc-mountpoints-dev,
-	libghc-libmagic-dev,
+	libghc-magic-dev,
 	libghc-socks-dev,
 	libghc-vector-dev,
 	libghc-unliftio-core-dev,
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 35a415330e..0f1e7361f1 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -100,6 +100,17 @@ and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
 
+### magic
+
+[magic](https://hackage.haskell.org/package/magic) since 1.1.1
+
+[First disclosed LLM generated code](https://github.com/philippedev101/magic-haskell/commit/f52a3628626644d68e1cd1d2c36e98f78693bda5)
+
+The person who took over magic from its long-time maintainer has not
+disclosed their earlier LLM use, but it is apparent, including in their
+communications to [[Joey]] about incorrect LLM-generated statements in the
+changelog.
+
 ### crypton
 
 [crypton](https://hackage.haskell.org/package/crypton) since 1.1.0
@@ -163,22 +174,3 @@ is a way that makes them not be a potential license problem.
 which was merged into gitk upstream of git and so avoided git's policy.
 
 git-annex supports git back to 2.22.
-
-## previous dependencies that contained LLM generated code
-
-[[!toc startlevel=3]]
-
-### magic
-
-[magic](https://hackage.haskell.org/package/magic) since 1.1.1
-

(Diff truncated)
comment
diff --git a/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment b/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
index 9f2f30a04b..db5e0b40f3 100644
--- a/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
+++ b/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
@@ -14,6 +14,14 @@ package, which has some very dodgy LLM generated code around filesystem
 encodings. I have not tested it.)
 
 git-annex has now switched to the haskell libmagic package. That fixes
-the problem on linux with LANG=C. The problem still exists on windows
-though. I think it needs fixing in libmagic probably.
+the problem on linux with LANG=C. 
+
+The problem still exists on windows though. I think it needs fixing in
+libmagic probably. Also libmagic caused a reversion, even plain ascii
+filenames now don't get handled right on windows. The test suite catches
+that too:
+
+	 magic:                              FAIL (10.51s)
+	      Test.hs:577:
+	      binary file not added to annex despite mimeencoding config
 """]]

comment
diff --git a/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment b/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
new file mode 100644
index 0000000000..9f2f30a04b
--- /dev/null
+++ b/doc/bugs/mimeencoding_detection_is_not_working/comment_4_3a796f6bbb8d4dce48f5f5b330d8dc1c._comment
@@ -0,0 +1,19 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 4"""
+ date="2026-10-27T14:33:54Z"
+ content="""
+The git-annex test suite got a test for this in 
+[[!commit 7fe5823d1747f7c2c1315dcc358cc600b096d58e]] and indeed it fails on
+windows. It also fails on linux with LANG=C.
+
+I think that the haskell magic package (pre 2.0.0) is not applying the
+filesystem encoding to the FilePath when passing it to the FFI, since
+it uses `withCString`. (I have my doubts about version 2.0.0 of that
+package, which has some very dodgy LLM generated code around filesystem
+encodings. I have not tested it.)
+
+git-annex has now switched to the haskell libmagic package. That fixes
+the problem on linux with LANG=C. The problem still exists on windows
+though. I think it needs fixing in libmagic probably.
+"""]]

Merge branch 'libmagic'
test case for magic with unicode filename
This test is known to fail on linux with LANG=C when git-annex is built
with magic. With libmagic, it succeeds. This does seem to indicate that
magic has a encoding issue, perhaps it fails to apply the filesystem
encoding when sending a FilePath across the FFI? I'm committing this
despite that problem because it *is* a problem and because I want to see
where else it might fail. Particularly, does it fail on Windows?
Note that git-annex add is not passed a list of the files, because with
LANG=C the unicode filename gets mangled somehow and does not get added.
Letting it add all files it sees works. This might point to a bug
somewhere (not involving magic), but it's probably not one that matters,
since with LANG=C the user does not expect to be passing unicode
filenames into commands.
diff --git a/Test.hs b/Test.hs
index e069f6c4a6..32d020e832 100644
--- a/Test.hs
+++ b/Test.hs
@@ -1,11 +1,12 @@
 {- git-annex test suite
  -
- - Copyright 2010-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE CPP #-}
 
 module Test where
@@ -27,6 +28,7 @@ import Control.Concurrent.STM hiding (check)
 import qualified Utility.RawFilePath as R
 import qualified Data.List.NonEmpty as NE
 import Data.String
+import System.OsString (osstr)
 
 import Common
 import CmdLine.GitAnnex.Options
@@ -564,16 +566,24 @@ test_magic = intmpclonerepo $ do
 #ifdef WITH_MAGICMIME
 	git "config" ["annex.largefiles", "mimeencoding=binary"]
 		"git config annex.largefiles"
-	writeFileString (literalOsPath "binary") "\127"
+	writeFileString (literalOsPath "binary") binarycontent
 	writeFileString (literalOsPath "text") "test\n" 
-	git_annex "add" ["binary", "text"]
+	writeFileString unicodefn binarycontent
+	git_annex "add" []
 		"git-annex add with mimeencoding in largefiles"
 	git_annex "sync" ["--no-content"]
 		"git-annex sync"
-	(isJust <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "binary")))
+	addedannex (literalOsPath "binary")
 		@? "binary file not added to annex despite mimeencoding config"
-	(isNothing <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "text")))
+	(not <$> addedannex (literalOsPath "text"))
 		@? "non-binary file got added to annex despite mimeencoding config"
+	addedannex unicodefn
+		@? "binary file with unicode name not added to annex despite mimeencoding config"
+  where
+	binarycontent = "\127"
+	-- quasiquoted because fromString for OsString mangles unicode
+	unicodefn = [osstr|fo¡oüঊ|]
+	addedannex f = isJust <$> annexeval (Annex.CatFile.catKeyFile f)
 #else
 	return ()
 #endif
diff --git a/doc/todo/libmagic.mdwn b/doc/todo/libmagic.mdwn
index 677d34ede6..fa1dad70c0 100644
--- a/doc/todo/libmagic.mdwn
+++ b/doc/todo/libmagic.mdwn
@@ -5,3 +5,6 @@ through git-annex to C libmagic correctly. Particularly on Windows
 with filenames that end up containing wide characters.
 There is some possibility of new problems, due to the change to OsPath.
 Seems low, but not tested yet. --[[Joey]]
+
+> Update: Added the test case, waiting to see if it works on windows with
+> magic before switching over. --[[Joey]]

test case for magic with unicode filename
Works on linux with LANG=C and unicode locale. Question is, does it work
on Windows?
Note that git-annex add is not passed a list of the files, because with
LANG=C the unicode filename gets mangled somehow and does not get added.
Letting it add all files it sees works. This might point to a bug
somewhere (not involving magic), but it's probably not one that matters,
since with LANG=C the user does not expect to be passing unicode
filenames into commands.
diff --git a/Test.hs b/Test.hs
index d2992dc25c..8937777b63 100644
--- a/Test.hs
+++ b/Test.hs
@@ -1,11 +1,12 @@
 {- git-annex test suite
  -
- - Copyright 2010-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE CPP #-}
 
 module Test where
@@ -27,6 +28,7 @@ import Control.Concurrent.STM hiding (check)
 import qualified Utility.RawFilePath as R
 import qualified Data.List.NonEmpty as NE
 import Data.String
+import System.OsString (osstr)
 
 import Common
 import CmdLine.GitAnnex.Options
@@ -562,16 +564,24 @@ test_magic = intmpclonerepo $ do
 #ifdef WITH_MAGICMIME
 	git "config" ["annex.largefiles", "mimeencoding=binary"]
 		"git config annex.largefiles"
-	writeFileString (literalOsPath "binary") "\127"
+	writeFileString (literalOsPath "binary") binarycontent
 	writeFileString (literalOsPath "text") "test\n" 
-	git_annex "add" ["binary", "text"]
+	writeFileString unicodefn binarycontent
+	git_annex "add" []
 		"git-annex add with mimeencoding in largefiles"
 	git_annex "sync" ["--no-content"]
 		"git-annex sync"
-	(isJust <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "binary")))
+	addedannex (literalOsPath "binary")
 		@? "binary file not added to annex despite mimeencoding config"
-	(isNothing <$> annexeval (Annex.CatFile.catKeyFile (literalOsPath "text")))
+	(not <$> addedannex (literalOsPath "text"))
 		@? "non-binary file got added to annex despite mimeencoding config"
+	addedannex unicodefn
+		@? "binary file with unicode name not added to annex despite mimeencoding config"
+  where
+	binarycontent = "\127"
+	-- quasiquoted because fromString for OsString mangles unicode
+	unicodefn = [osstr|fo¡oüঊ|]
+	addedannex f = isJust <$> annexeval (Annex.CatFile.catKeyFile f)
 #else
 	return ()
 #endif
diff --git a/doc/todo/libmagic.mdwn b/doc/todo/libmagic.mdwn
new file mode 100644
index 0000000000..9f297ff695
--- /dev/null
+++ b/doc/todo/libmagic.mdwn
@@ -0,0 +1,10 @@
+The `libmagic` branch switches to a fork of the magic library.
+
+Before merging it, need a test case that filenames travel end to end
+through git-annex to C libmagic correctly. Particularly on Windows
+with filenames that end up containing wide characters.
+There is some possibility of new problems, due to the change to OsPath.
+Seems low, but not tested yet. 
+
+> Update: Added the test case, waiting to see if it works on windows with
+> magic before switching over. --[[Joey]]

libmagic todo
diff --git a/doc/todo/libmagic.mdwn b/doc/todo/libmagic.mdwn
new file mode 100644
index 0000000000..677d34ede6
--- /dev/null
+++ b/doc/todo/libmagic.mdwn
@@ -0,0 +1,7 @@
+The `libmagic` branch switches to a fork of the magic library.
+
+Before merging it, need a test case that filenames travel end to end
+through git-annex to C libmagic correctly. Particularly on Windows
+with filenames that end up containing wide characters.
+There is some possibility of new problems, due to the change to OsPath.
+Seems low, but not tested yet. --[[Joey]]

arm64-ancient EOL warnings
diff --git a/CHANGELOG b/CHANGELOG
index 48be3f89fe..294306b26c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,11 @@
+git-annex (10.20261027) UNRELEASED; urgency=medium
+
+  * The Linux arm64-ancient build has reached end of security support 
+    and will be removed after this release. Android users are recommended
+    to switch to running `pkg install git-annex` inside termux.
+
+ -- Joey Hess <id@joeyh.name>  Mon, 26 Oct 2026 18:34:32 -0400
+
 git-annex (10.20261026) upstream; urgency=high
 
   * This releases fixes a security hole that allowed private data 
diff --git a/doc/install/Linux_standalone.mdwn b/doc/install/Linux_standalone.mdwn
index 83bae6af9b..b1313bd418 100644
--- a/doc/install/Linux_standalone.mdwn
+++ b/doc/install/Linux_standalone.mdwn
@@ -9,7 +9,8 @@ dependencies and is self-contained.
 * x86-32: [download tarball](https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-i386.tar.gz)
 * arm: [download tarball](https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-armhf.tar.gz)
 * arm64: [download tarball](https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-arm64.tar.gz)
-* arm64, for ancient kernels: [download tarball](https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-arm64-ancient.tar.gz)
+* arm64, for ancient kernels: [download tarball](https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-arm64-ancient.tar.gz)  
+  (This build is end of life and will be removed soon.)
 
 To use, just unpack the tarball, `cd git-annex.linux` and run `./runshell`
 -- this sets up an environment where you can use `git annex`, as well
@@ -36,8 +37,9 @@ An hourly autobuild is also available, hosted by [[Joey]]:
 * x86-64: [download tarball](https://downloads.kitenet.net/git-annex/autobuild/amd64/git-annex-standalone-amd64.tar.gz) ([build logs](https://downloads.kitenet.net/git-annex/autobuild/amd64/))
 * x86-32: [download tarball](https://downloads.kitenet.net/git-annex/autobuild/i386/git-annex-standalone-i386.tar.gz) ([build logs](https://downloads.kitenet.net/git-annex/autobuild/i386/))
 * arm64: [download tarball](https://downloads.kitenet.net/git-annex/autobuild/arm64/git-annex-standalone-arm64.tar.gz) ([build logs](https://downloads.kitenet.net/git-annex/autobuild/arm64/))
-* arm64, for ancient kernels: [download tarball](https://downloads.kitenet.net/git-annex/autobuild/arm64-ancient/git-annex-standalone-arm64-ancient.tar.gz) ([build logs](https://downloads.kitenet.net/git-annex/autobuild/arm64-ancient/))
 * arm: [download tarball](https://downloads.kitenet.net/git-annex/autobuild/armhf/git-annex-standalone-armhf.tar.gz) ([build logs](https://downloads.kitenet.net/git-annex/autobuild/armhf/))
+* arm64, for ancient kernels: [download tarball](https://downloads.kitenet.net/git-annex/autobuild/arm64-ancient/git-annex-standalone-arm64-ancient.tar.gz) ([build logs](https://downloads.kitenet.net/git-annex/autobuild/arm64-ancient/))  
+  (This build is end of life and will be removed soon.)
 
 ## download security
 
diff --git a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
index f688beb8c4..c98befa964 100644
--- a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
+++ b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
@@ -18,4 +18,8 @@ worth doing a build just for old android phones any longer. --[[Joey]]
 
 > > > I made a `remove-arm64ancient` branch for this. Waiting to merge it
 > > > until after one last security update release with the ca-certificates.
+> > > 
+> > > Update: And with that out of the way, I think waiting 1 more release
+> > > makes sense, so documented that it is end of life but didn't remove
+> > > yet.
 > > > --[[Joey]]

supply chain attack
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 7b00cd43a9..0f1e7361f1 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -28,6 +28,11 @@ 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.
 
+As the review has progressed, it has became clear that LLM generated code
+in dependencies also needs to be viewed as a potential supply chain attack.
+While the risk varies, review has found some very serious problems,
+including a confirmed license violation, and security risks.
+
 Additional work needs to be done on an ongoing basis to review
 git-annex's dependencies to detect the addition of LLM generated code.
 

formatting fix
diff --git a/doc/news/version_10.20261026.mdwn b/doc/news/version_10.20261026.mdwn
index 3abfb5cfea..434a2dd67e 100644
--- a/doc/news/version_10.20261026.mdwn
+++ b/doc/news/version_10.20261026.mdwn
@@ -1,16 +1,13 @@
 News for git-annex 10.20261026:
 
-git-annex (10.20261026) upstream; urgency=high
-.
-  A security fix has changed git-annex's behavior in several ways when
-  enabling special remotes. The new git-annex verify command needs to be
-  used to verify that the configuration of a special remote is safe.
-  git-annex will prompt in situations where that is needed.
-.
-  Also, changes to a special remote configuration received in the git-annex
-  branch no longer take effect until git-annex verifyremote is run to review
-  and verify the new configuration.
+A security fix has changed git-annex's behavior in several ways when
+enabling special remotes. The new git-annex verify command needs to be
+used to verify that the configuration of a special remote is safe.
+git-annex will prompt in situations where that is needed.
 
+Also, changes to a special remote configuration received in the git-annex
+branch no longer take effect until git-annex verifyremote is run to review
+and verify the new configuration.
 
 git-annex 10.20261026 released with [[!toggle text="these changes"]]
 [[!toggleable text="""  * This releases fixes a security hole that allowed private data
@@ -39,4 +36,4 @@ git-annex 10.20261026 released with [[!toggle text="these changes"]]
     forced.
   * Added remote.name.annex-verified git config.
   * git-annex.cabal: Added Memory build flag to support building with
-    older versions of crypton that don't use ram."""]]
\ No newline at end of file
+    older versions of crypton that don't use ram."""]]

add news item for git-annex 10.20261026
diff --git a/doc/news/version_10.20260901.mdwn b/doc/news/version_10.20260901.mdwn
deleted file mode 100644
index 9ae064b54a..0000000000
--- a/doc/news/version_10.20260901.mdwn
+++ /dev/null
@@ -1,35 +0,0 @@
-git-annex 10.20260901 released with [[!toggle text="these changes"]]
-[[!toggleable text="""  * Behavior change: drop --auto --from a remote does not any longer
-    try to drop content that is not known to be present on the remote.
-    This avoids unncessary work and makes it consistent with the behavior
-    of git-annex sync and push.
-  * External special remote protocol extended to support IMPORTKEY.
-  * git-annex-remote-internetarchive supports --no-content imports.
-  * diffdriver: Avoid crashing when git passes an (undocumented) 8th
-    parameter.
-  * The preferred content groupwanted expression will no longer
-    consider a groupwanted expression of "" to be set, which allows
-    another group's groupwanted expression to be used instead.
-  * Fixed buggy handling of preferred content
-    "balanced=groupname:lackingcopies"
-  * Expand preferred content "lackingcopies" and "approxlackingcopies"
-    expression syntax to support "groupname:number"
-  * Expand preferred content "copies", "lackingcopies", and
-    "approxlackingcopies" expression syntax to support group limits which
-    can include/exclude multiple groups. Eg
-    "copies=archive+backup-offsite=3"
-  * Also expanded --lackingcopies, --approxlackingcopies, and --copies
-    with the same syntax.
-  * Expand preferred content "balanced", "fullybalanced",
-    "sizebalanced" and "fullysizebalanced" expression syntax to support
-    group limits as well. Eg
-    "balanced=backup:lackingcopies=archive-offsite"
-  * importfeed: Fix reporting and logging of problems with feeds.
-  * importfeed: When adding an url, indicate which feed it is from.
-  * Fix reversion in 8.20200226 that broke git-annex benchmark --databases
-  * Remove the ParallelBuild cabal flag and add cabal.project that
-    enables parallel build by default with ghc 9.8+ and cabal-install 3.12.
-  * NoLLMDependencies: Update for warp and magic.
-  * git-annex.cabal: Pin magic to 1.1 avoiding build failure on Windows
-    with newer version.
-  * stack.yaml: Update to lts-24.52"""]]
\ No newline at end of file
diff --git a/doc/news/version_10.20261026.mdwn b/doc/news/version_10.20261026.mdwn
new file mode 100644
index 0000000000..3abfb5cfea
--- /dev/null
+++ b/doc/news/version_10.20261026.mdwn
@@ -0,0 +1,42 @@
+News for git-annex 10.20261026:
+
+git-annex (10.20261026) upstream; urgency=high
+.
+  A security fix has changed git-annex's behavior in several ways when
+  enabling special remotes. The new git-annex verify command needs to be
+  used to verify that the configuration of a special remote is safe.
+  git-annex will prompt in situations where that is needed.
+.
+  Also, changes to a special remote configuration received in the git-annex
+  branch no longer take effect until git-annex verifyremote is run to review
+  and verify the new configuration.
+
+
+git-annex 10.20261026 released with [[!toggle text="these changes"]]
+[[!toggleable text="""  * This releases fixes a security hole that allowed private data
+    exfiltration from special remotes. (CVE-2026-112818)
+    https://git-annex.branchable.com/security/cross\_repository\_data\_exfiltration\_via\_special\_remote/
+  * verifyremote: New command, used to verify the configuration of special
+    remotes is safe.
+  * enableremote: Display configuration and prompt for verifyremote to be
+    run. Until it is verified, the special remote cannot be used,
+    and git pushes to all remotes will be prevented.
+  * When a special remote has been autoenabled, allow it to be used without
+    verification, but prevent sending annexed files or git pushes to any
+    remote until it is verified.
+  * After upgrading git-annex, existing special remotes will continue to work
+    as before. If a special remote could have been configured by a third
+    party, you should verify its configuration using the
+    `git-annex verifyremote` command.
+  * Changes to special remote configuration received in the git-annex
+    branch no longer take effect until git-annex verifyremote is run to
+    review and verify the new configuration.
+  * init: When run in an already initialized repository, avoid
+    autoenabling new special remotes, since files received from
+    them could leak out via other git-annex processes running in the
+    repository.
+  * init --autoenable: Refuse to perform probably unsafe action unless
+    forced.
+  * Added remote.name.annex-verified git config.
+  * git-annex.cabal: Added Memory build flag to support building with
+    older versions of crypton that don't use ram."""]]
\ No newline at end of file

response
diff --git a/doc/forum/recovering_a_git_repository_from_encrypted_remote/comment_1_191a343758b4575e996a01bc88726b68._comment b/doc/forum/recovering_a_git_repository_from_encrypted_remote/comment_1_191a343758b4575e996a01bc88726b68._comment
new file mode 100644
index 0000000000..2b4c605740
--- /dev/null
+++ b/doc/forum/recovering_a_git_repository_from_encrypted_remote/comment_1_191a343758b4575e996a01bc88726b68._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-26T14:41:38Z"
+ content="""
+Sure, if you prime the git-annex branch with the remote.log entry for
+the remote, git-annex has enough information to access the remote.
+
+So it will work, but you still can't clone it directly.
+"""]]

deslop
diff --git a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
index a19352c124..a53e02f37a 100644
--- a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
+++ b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
@@ -35,35 +35,9 @@ git-annex version: 10.20260421 on NixOS/Linux.
 
 Claude produced this patch, which I don't understand, but when running "git annex get" with this patch succeeds, so maybe it is useful.
 
-```patch
---- a/P2P/Protocol.hs	2026-10-26 12:16:18.311257773 +0100
-+++ b/P2P/Protocol.hs	2026-10-26 12:16:29.446642504 +0100
-@@ -221,14 +221,17 @@
- -- unlikely to care about spaces vs tabs or control characters.
- instance Proto.Serializable ProtoAssociatedFile where
- 	serialize (ProtoAssociatedFile (AssociatedFile Nothing)) = ""
--	serialize (ProtoAssociatedFile (AssociatedFile (Just af))) = 
--		fromOsPath $ toInternalGitPath $
--			OS.concat $ map esc $ OS.unpack af
-+	serialize (ProtoAssociatedFile (AssociatedFile (Just af))) =
-+		-- Escape after decoding to unicode, not per byte. The peer
-+		-- splits the line with a unicode isSpace, so multibyte
-+		-- whitespace (eg U+3000) must be escaped, while utf-8
-+		-- continuation bytes (eg 0xA0, 0x85) must not be.
-+		concatMap esc (fromOsPath (toInternalGitPath af) :: String)
- 	  where
--	  	esc c = case OS.toChar c of
--			'%' -> literalOsPath "%%"
--			c' | isSpace c' -> literalOsPath "%"
--			_ -> OS.singleton c
-+	  	esc '%' = "%%"
-+		esc c
-+			| isSpace c = "%"
-+			| otherwise = [c]
- 	
- 	deserialize s = case fromInternalGitPath $ toOsPath $ deesc [] s of
- 		f
-```
+> I understand why you included that, but git-annex has a no LLM generated
+> code policy, so I have deleted it. It's the wrong fix in any case. I'll
+> fix it ASAP. --[[Joey]]
 
 ### 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)
 

git-annex.cabal: Switch from magic to libmagic
There are enough differences between magic-1.1 and libmagic-1.2 that I
don't want to ifdef between them. And git-annex will never support
building with magic > 1.1 as it is full of LLM slop.
diff --git a/Annex/Magic.hs b/Annex/Magic.hs
index 4771adada4..e455a563fc 100644
--- a/Annex/Magic.hs
+++ b/Annex/Magic.hs
@@ -1,6 +1,6 @@
 {- Interface to libmagic
  -
- - Copyright 2019-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -23,8 +23,7 @@ import Control.Monad.IO.Class
 #ifdef WITH_MAGICMIME
 import Magic
 import Utility.Env
-import Control.Concurrent
-import System.IO.Unsafe (unsafePerformIO)
+import qualified Data.List.NonEmpty as NE
 #else
 type Magic = ()
 #endif
@@ -35,7 +34,7 @@ initMagicMime = catchMaybeIO $ do
 	m <- magicOpen [MagicMime]
 	liftIO $ getEnv "GIT_ANNEX_DIR" >>= \case
 		Nothing -> magicLoadDefault m
-		Just d -> magicLoad m $ fromOsPath $
+		Just d -> magicLoad m $ NE.singleton $
 			toOsPath d
 				</> literalOsPath "magic"
 				</> literalOsPath "magic.mgc"
@@ -46,7 +45,7 @@ initMagicMime = return Nothing
 
 getMagicMime :: Magic -> OsPath -> IO (Maybe (MimeType, MimeEncoding))
 #ifdef WITH_MAGICMIME
-getMagicMime m f = Just . parse <$> magicConcurrentSafe (magicFile m (fromOsPath f))
+getMagicMime m f = Just . parse <$> magicFile m f
   where
 	parse s = 
 		let (mimetype, rest) = separate (== ';') s
@@ -63,15 +62,3 @@ getMagicMimeType m f = liftIO $ fmap fst <$> getMagicMime m f
 
 getMagicMimeEncoding :: MonadIO m => Magic -> OsPath -> m(Maybe MimeEncoding)
 getMagicMimeEncoding m f = liftIO $ fmap snd <$> getMagicMime m f
-
-#ifdef WITH_MAGICMIME
-{-# NOINLINE mutex #-}
-mutex :: MVar ()
-mutex = unsafePerformIO $ newMVar ()
-
--- Work around a bug, the library is not concurrency safe and will
--- sometimes access the wrong memory if multiple ones are called at the
--- same time.
-magicConcurrentSafe :: IO a -> IO a
-magicConcurrentSafe = bracket_ (takeMVar mutex) (putMVar mutex ())
-#endif
diff --git a/CHANGELOG b/CHANGELOG
index f79be154d4..62529ca015 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,7 @@ git-annex (10.20261021) UNRELEASED; urgency=medium
 
   * git-annex.cabal: Added Memory build flag to support building with
     older versions of crypton that don't use ram.
+  * git-annex.cabal: Switch from magic to libmagic.
 
  -- Joey Hess <id@joeyh.name>  Wed, 21 Oct 2026 10:38:20 -0400
 
diff --git a/debian/control b/debian/control
index 3c80b61f48..93368f0656 100644
--- a/debian/control
+++ b/debian/control
@@ -77,7 +77,7 @@ Build-Depends:
 	libghc-unbounded-delays-dev,
 	libghc-disk-free-space-dev,
 	libghc-mountpoints-dev,
-	libghc-magic-dev,
+	libghc-libmagic-dev,
 	libghc-socks-dev,
 	libghc-vector-dev,
 	libghc-unliftio-core-dev,
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 7b00cd43a9..2b42d98e93 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -95,17 +95,6 @@ and 10,000+ lines of changes.
 
 (See [[todo/ditch_yesod]])
 
-### magic
-
-[magic](https://hackage.haskell.org/package/magic) since 1.1.1
-
-[First disclosed LLM generated code](https://github.com/philippedev101/magic-haskell/commit/f52a3628626644d68e1cd1d2c36e98f78693bda5)
-
-The person who took over magic from its long-time maintainer has not
-disclosed their earlier LLM use, but it is apparent, including in their
-communications to [[Joey]] about incorrect LLM-generated statements in the
-changelog.
-
 ### crypton
 
 [crypton](https://hackage.haskell.org/package/crypton) since 1.1.0
@@ -169,3 +158,22 @@ is a way that makes them not be a potential license problem.
 which was merged into gitk upstream of git and so avoided git's policy.
 
 git-annex supports git back to 2.22.
+
+## previous dependencies that contained LLM generated code
+
+[[!toc startlevel=3]]
+
+### magic
+
+[magic](https://hackage.haskell.org/package/magic) since 1.1.1
+
+[First disclosed LLM generated code](https://github.com/philippedev101/magic-haskell/commit/f52a3628626644d68e1cd1d2c36e98f78693bda5)
+
+The person who took over magic from its long-time maintainer has not
+disclosed their earlier LLM use, but it is apparent, including in their
+communications to [[Joey]] about incorrect LLM-generated statements in the
+changelog.
+
+This resulted in the package being forked to 
+[libmagic](https://hackage.haskell.org/package/libmagic),
+which git-annex now uses instead.
diff --git a/git-annex.cabal b/git-annex.cabal
index edb5a9a627..e7af353763 100644
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -551,7 +551,7 @@ Executable git-annex
     CPP-Options: -DWITH_TORRENTPARSER
 
   if flag(MagicMime)
-    Build-Depends: magic (<= 1.1)
+    Build-Depends: libmagic
     CPP-Options: -DWITH_MAGICMIME
 
   if flag(Benchmark)
diff --git a/stack-NoLLMDependencies.yaml b/stack-NoLLMDependencies.yaml
index 8d2f3ee7b9..1a5d387901 100644
--- a/stack-NoLLMDependencies.yaml
+++ b/stack-NoLLMDependencies.yaml
@@ -28,6 +28,6 @@ extra-deps:
 - persistent-sqlite-2.13.3.0
 - yesod-form-1.7.9.2
 - yesod-static-1.6.1.2
-- magic-1.1
+- libmagic-1.2
 - crypton-1.0.6
 - tls-2.1.8
diff --git a/stack-botan.yaml b/stack-botan.yaml
index 7e5794f6d1..6f2261b026 100644
--- a/stack-botan.yaml
+++ b/stack-botan.yaml
@@ -25,5 +25,5 @@ extra-deps:
 - botan-low-0.2.0.1
 - botan-bindings-0.3.0.0
 - blake3-0.3
-- magic-1.1
+- libmagic-1.2
 - ram-0.22.0
diff --git a/stack.yaml b/stack.yaml
index c8e26111bb..fa3adbf5d4 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -23,5 +23,5 @@ extra-deps:
 - aws-0.25.2
 - file-io-0.2.0
 - blake3-0.3
-- magic-1.1
+- libmagic-1.2
 - ram-0.22.0

diff --git a/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
new file mode 100644
index 0000000000..a19352c124
--- /dev/null
+++ b/doc/bugs/SSH_remote_get_fails_with_non-ASCII_whitespace.mdwn
@@ -0,0 +1,71 @@
+### Please describe the problem in your own words.
+
+Trying to "git annex get" from a SSH remote can fail when filenames contain some Unicode characters such as U+3000 IDEOGRAPHIC SPACE, which is the default whitespace character when inputting Japanese text.
+
+### What steps will reproduce the problem?
+
+```
+export GIT_AUTHOR_EMAIL=test@example.com GIT_COMMITTER_EMAIL=test@example.com
+git init a; cd a; git annex init
+echo x > 'foo bar'   # U+3000 between foo and bar
+git annex add; git commit -m add; cd ..
+git clone ssh://localhost$PWD/a b; cd b; git annex init
+git annex get .
+```
+
+The last command will fail with
+
+```
+get "foo\343\200\200bar" (from origin...)
+
+  Transfer failed
+
+  Unable to access these remotes: origin
+
+  No other repository is known to contain the file.
+failed
+get: 1 failed
+```
+
+### What version of git-annex are you using? On what operating system?
+
+git-annex version: 10.20260421 on NixOS/Linux.
+
+### Please provide any additional information below.
+
+Claude produced this patch, which I don't understand, but when running "git annex get" with this patch succeeds, so maybe it is useful.
+
+```patch
+--- a/P2P/Protocol.hs	2026-10-26 12:16:18.311257773 +0100
++++ b/P2P/Protocol.hs	2026-10-26 12:16:29.446642504 +0100
+@@ -221,14 +221,17 @@
+ -- unlikely to care about spaces vs tabs or control characters.
+ instance Proto.Serializable ProtoAssociatedFile where
+ 	serialize (ProtoAssociatedFile (AssociatedFile Nothing)) = ""
+-	serialize (ProtoAssociatedFile (AssociatedFile (Just af))) = 
+-		fromOsPath $ toInternalGitPath $
+-			OS.concat $ map esc $ OS.unpack af
++	serialize (ProtoAssociatedFile (AssociatedFile (Just af))) =
++		-- Escape after decoding to unicode, not per byte. The peer
++		-- splits the line with a unicode isSpace, so multibyte
++		-- whitespace (eg U+3000) must be escaped, while utf-8
++		-- continuation bytes (eg 0xA0, 0x85) must not be.
++		concatMap esc (fromOsPath (toInternalGitPath af) :: String)
+ 	  where
+-	  	esc c = case OS.toChar c of
+-			'%' -> literalOsPath "%%"
+-			c' | isSpace c' -> literalOsPath "%"
+-			_ -> OS.singleton c
++	  	esc '%' = "%%"
++		esc c
++			| isSpace c = "%"
++			| otherwise = [c]
+ 	
+ 	deserialize s = case fromInternalGitPath $ toOsPath $ deesc [] s of
+ 		f
+```
+
+### 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)
+
+I've spend the last few days migrating *everything* to git annex, it's wonderful!
+

todo
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 4b708087ba..7b00cd43a9 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -142,15 +142,6 @@ including `tls`.
 
 git-annex supports being built with older versions.
 
-tls is used by http-client-tls and warp-tls, both of which git-annex
-depends on. It would be possible to switch to http-client-openssl
-and warp-s2n-tls to avoid tls as well as crypton. Note that openssl
-itself contains LLM generated code, as does warp-s2n-tls.
-This still may be worth doing though, to get away from crypton.
-
-But aws and DAV also use http-client-tls, so that wouldn't get away from it
-entirely.
-
 ### Cabal
 
 [First LLM generated code](https://github.com/haskell/cabal/commit/da8b314563feb15a3df7bc1baeef4b7aa08f7578)
diff --git a/doc/todo/remove_dependency_on_crypon_and_tls.mdwn b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
new file mode 100644
index 0000000000..a2982187b6
--- /dev/null
+++ b/doc/todo/remove_dependency_on_crypon_and_tls.mdwn
@@ -0,0 +1,37 @@
+The current developer (not the original author) of crypton and tls
+[states](https://kazu-yamamoto.hatenablog.jp/entry/2026/09/25/110313)
+that "I am not a cryptography expert, so I cannot write code that is
+both correct and performant." This begs the question: Does anyone
+understand and has anyone reviewed the LLM generated code that they are
+pouring into these libraries?
+
+The point of cryptonite was to package up industry standard C
+implementations of crypto functions in haskell. This is no longer
+that, and as such, does not seem like a trustworthly package to use.
+
+The point of the tls library is well, to be secure. It seems doubtful that
+the haskell tls library is better than standard tls libraries like openssl
+at this point in time.
+
+I have lost confidence in the maintenance of these, and would like
+git-annex to migrate away from depending on them. --[[Joey]] 
+
+This is separate, though obviously not entirely disconnected from
+[[/no_LLM_code]].
+
+-----
+
+Here is a plan:
+
+* Switch git-annex to botan for hash functions. There is already a build
+  flag, but it would be easier if botan bundled the C library in the
+  haskell package. Which they have considered doing.
+* botan lacks skein and blake2, so would need to get those hashes another
+  way
+* Switch from http-client-tls to http-client-openssl.
+* Switch from warp-tls to warp-s2n-tls? Alternatively, warp-tls is only used by
+  the webapp and p2phttp; the webapp is mostly used on localhost and
+  p2phttp is often used behind a server (eg forgejo) that can handle the
+  https if it only supported http.
+* Make aws somehow able to use http-client-openssl (eg, by parameterizing it)
+* Make DAV somehow able to use http-client-openssl (eg, by parameterizing it)

more on tls
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 1a279cc7aa..4b708087ba 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -148,6 +148,9 @@ and warp-s2n-tls to avoid tls as well as crypton. Note that openssl
 itself contains LLM generated code, as does warp-s2n-tls.
 This still may be worth doing though, to get away from crypton.
 
+But aws and DAV also use http-client-tls, so that wouldn't get away from it
+entirely.
+
 ### Cabal
 
 [First LLM generated code](https://github.com/haskell/cabal/commit/da8b314563feb15a3df7bc1baeef4b7aa08f7578)

clarify
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 5d80b64997..1a279cc7aa 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -128,7 +128,8 @@ implementations of crypto functions in haskell. This is no longer
 that, and as such, does not seem like a trustworthly package to use.
 
 git-annex supports being built with older versions. The botan build flag
-makes git-annex use botan rather than crypton.
+makes git-annex use botan rather than crypton for hashing (except for skein
+and blake2 hashes).
 
 Unfortunately, crypton is a dependency of several other packages,
 including `tls`. 

tls alternatives
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index e05f444230..5d80b64997 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -141,6 +141,12 @@ including `tls`.
 
 git-annex supports being built with older versions.
 
+tls is used by http-client-tls and warp-tls, both of which git-annex
+depends on. It would be possible to switch to http-client-openssl
+and warp-s2n-tls to avoid tls as well as crypton. Note that openssl
+itself contains LLM generated code, as does warp-s2n-tls.
+This still may be worth doing though, to get away from crypton.
+
 ### Cabal
 
 [First LLM generated code](https://github.com/haskell/cabal/commit/da8b314563feb15a3df7bc1baeef4b7aa08f7578)

update re ram
diff --git a/doc/no_llm_code.mdwn b/doc/no_llm_code.mdwn
index 504c0e38ae..e05f444230 100644
--- a/doc/no_llm_code.mdwn
+++ b/doc/no_llm_code.mdwn
@@ -52,18 +52,15 @@ 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
 
 [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 use ram, git-annex continues to use the unmaintained
-[memory](//hackage.haskell.org/package/memory) that ram was forked from.
-
-But ram is an dependency of other dependencies, and these in particular
+ram is an dependency of other dependencies, and these in particular
 depend on 0.21.0 or newer:
 
 * tls since 2.3.1

add fix version
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
index 5368ecef17..37e621ebbf 100644
--- a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -2,7 +2,7 @@ CVE-2026-112818: git-annex was vulnerable to a private data exfiltration attack.
 
 Joey Hess discovered this attack.
 
-This security hole was fixed in git-annex 
+This security hole was fixed in git-annex 10.20261026.
 
 ## attack descriptions
 

diff --git a/doc/forum/recovering_a_git_repository_from_encrypted_remote.mdwn b/doc/forum/recovering_a_git_repository_from_encrypted_remote.mdwn
new file mode 100644
index 0000000000..d841a37a22
--- /dev/null
+++ b/doc/forum/recovering_a_git_repository_from_encrypted_remote.mdwn
@@ -0,0 +1,13 @@
+The manual warns against pushing into encrypted remote because:
+
+>> it is impossible to git clone from an encrypted special remote, since it needs encryption keys stored in the remote.
+
+However am I correct that if store "uuid" "cipher" and all relevant info from the remote log, I can recover a git repository by:
+
+1. `git init`
+2. create a git-annex branch with one entry in the remote.log
+3. `git-annex init`
+4. `git annex enableremote --with-url myremote`
+4. and then pull?
+
+I tried it locally and it works but I was wondering if I'm missing something that would make it dangerous to rely on this process.

prepare for release
diff --git a/doc/bugs/sync_--rebalance_overshoots.mdwn b/doc/bugs/sync_--rebalance_overshoots.mdwn
new file mode 100644
index 0000000000..f497388af2
--- /dev/null
+++ b/doc/bugs/sync_--rebalance_overshoots.mdwn
@@ -0,0 +1,139 @@
+### Please describe the problem in your own words.
+
+In a `sizebalanced` group, `git annex sync --content --rebalance` does not
+stop at an even split. It keeps moving files off the fullest repository
+until that repository is almost empty, and the next run moves nearly
+everything back.
+
+
+### What steps will reproduce the problem?
+
+Two repos in group `bal`, 20 files of 1 MB, all starting in `a`:
+
+[[!format sh """
+#!/bin/sh
+set -e
+git init -q a && cd a && git annex init -q a
+for i in $(seq -w 1 20); do head -c 1000000 /dev/urandom > f$i; done
+git annex add -q . && git commit -qm add
+cd .. && git clone -q a b && (cd b && git annex init -q b)
+cd a
+git remote add b ../b
+for r in here b; do
+	git annex group $r bal
+	git annex wanted $r groupwanted
+	git annex maxsize $r 100MB
+done
+git annex groupwanted bal sizebalanced=bal
+git annex sync --content --rebalance
+echo "files in a: $(git annex find --in here | wc -l), in b: $(git annex find --in b | wc -l)"
+"""]]
+
+Output ends with `files in a: 0, in b: 20`. Expected about 10 in each.
+
+
+### What version of git-annex are you using? On what operating system?
+
+git-annex 10.20261020 (Arch Linux package 10.20261020-2), Linux 7.2.9.
+
+### 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
+
+# Output of the script above:
+$ sh repro.sh
+group here ok
+(recording state in git...)
+wanted here ok
+(recording state in git...)
+maxsize here ok
+(recording state in git...)
+group b ok
+(recording state in git...)
+wanted b ok
+(recording state in git...)
+maxsize b ok
+(recording state in git...)
+groupwanted bal ok
+(recording state in git...)
+commit
+On branch master
+nothing to commit, working tree clean
+ok
+pull b
+From ../b
+ * [new branch]      git-annex  -> b/git-annex
+ * [new branch]      master     -> b/master
+ok
+(merging b/git-annex into git-annex...)
+(recording state in git...)
+(calculating repository sizes...)
+put f01 (to b...) ok
+drop f01 ok
+put f02 (to b...) ok
+drop f02 ok
+put f03 (to b...) ok
+drop f03 ok
+put f04 (to b...) ok
+drop f04 ok
+put f05 (to b...) ok
+drop f05 ok
+put f06 (to b...) ok
+drop f06 ok
+put f07 (to b...) ok
+drop f07 ok
+put f08 (to b...) ok
+drop f08 ok
+put f09 (to b...) ok
+drop f09 ok
+put f10 (to b...) ok
+drop f10 ok
+put f11 (to b...) ok
+drop f11 ok
+put f12 (to b...) ok
+drop f12 ok
+put f13 (to b...) ok
+drop f13 ok
+put f14 (to b...) ok
+drop f14 ok
+put f15 (to b...) ok
+drop f15 ok
+put f16 (to b...) ok
+drop f16 ok
+put f17 (to b...) ok
+drop f17 ok
+put f18 (to b...) ok
+drop f18 ok
+put f19 (to b...) ok
+drop f19 ok
+put f20 (to b...) ok
+drop f20 ok
+pull b
+ok
+(recording state in git...)
+push b
+Everything up-to-date
+remote: (merging synced/git-annex into git-annex...)
+remote: (recording state in git...)
+To ../b
+ * [new branch]      git-annex -> synced/git-annex
+ok
+files in a: 0, in b: 20
+
+# End of transcript or log.
+"""]]
+
+I hit this with five USB drives in a sizebalanced group.
+
+
+### 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)
+
+Yes, it manages a few TB across six drives here. I like that I can see all my
+files on my computer without having all the external drives attached, or having
+to remember which drive a file is on. I also like that it takes care of my
+backups at the same time, with the numcopies setting. Lately I've been testing
+the new balanced preferred content on my USB drives, which is how I found this.
+
+

add news item for git-annex 10.20261020
diff --git a/doc/news/version_10.20260717.mdwn b/doc/news/version_10.20260717.mdwn
deleted file mode 100644
index 37abe8d757..0000000000
--- a/doc/news/version_10.20260717.mdwn
+++ /dev/null
@@ -1,18 +0,0 @@
-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
diff --git a/doc/news/version_10.20261020.mdwn b/doc/news/version_10.20261020.mdwn
new file mode 100644
index 0000000000..d620da61a3
--- /dev/null
+++ b/doc/news/version_10.20261020.mdwn
@@ -0,0 +1,8 @@
+git-annex 10.20261020 released with [[!toggle text="these changes"]]
+[[!toggleable text="""  * adjust --hide-unwanted: New option to make an adjusted branch
+    containing only wanted files.
+  * push: Force push to remote git-annex branch when it has been
+    supersceded by a transition.
+  * Fix handling of --sameas special remotes with onlyencryptcreds=yes.
+  * Linux standalone builds bundled CA certs updated for
+    EMudhra Intermediate CA Compromise."""]]
\ No newline at end of file

branch
diff --git a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
index 1e61073b25..f688beb8c4 100644
--- a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
+++ b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
@@ -15,3 +15,7 @@ worth doing a build just for old android phones any longer. --[[Joey]]
 
 > > Temporarily dealt with that by manually removing the affected cert from
 > > the arm64ancient build chroot. --[[Joey]]
+
+> > > I made a `remove-arm64ancient` branch for this. Waiting to merge it
+> > > until after one last security update release with the ca-certificates.
+> > > --[[Joey]]

update
diff --git a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
index dd8e69edc1..1e61073b25 100644
--- a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
+++ b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
@@ -12,3 +12,6 @@ worth doing a build just for old android phones any longer. --[[Joey]]
 > If ca-certificates won't be updated for the EMudhra Intermediate CA Compromise,
 > it would be best to either drop the build or release a new build that
 > does not bundle any certs. --[[Joey]]
+
+> > Temporarily dealt with that by manually removing the affected cert from
+> > the arm64ancient build chroot. --[[Joey]]

update
diff --git a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
index dffd108531..dd8e69edc1 100644
--- a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
+++ b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
@@ -8,3 +8,7 @@ may not work with that kernel.
 
 It may be that, since termux now includes a git-annex package, it's not
 worth doing a build just for old android phones any longer. --[[Joey]]
+
+> If ca-certificates won't be updated for the EMudhra Intermediate CA Compromise,
+> it would be best to either drop the build or release a new build that
+> does not bundle any certs. --[[Joey]]

close old xmpp bugs
diff --git a/doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably.mdwn b/doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably.mdwn
index a6c258332a..9b112d0eef 100644
--- a/doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably.mdwn
+++ b/doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably.mdwn
@@ -24,3 +24,5 @@ There is no logoutput to add... I'm sorry.
 [[!meta title="XMPP does not work with jabber.ccc.de"]]
 
 [[!tag moreinfo]]
+
+> [[done]]; git-annex removed xmpp support in 6.20170101 --[[Joey]]
diff --git a/doc/bugs/can__39__t_connect_jabber_with_custom_google_apps_domain.mdwn b/doc/bugs/can__39__t_connect_jabber_with_custom_google_apps_domain.mdwn
index b63f56ce83..89af18803e 100644
--- a/doc/bugs/can__39__t_connect_jabber_with_custom_google_apps_domain.mdwn
+++ b/doc/bugs/can__39__t_connect_jabber_with_custom_google_apps_domain.mdwn
@@ -19,3 +19,5 @@ The issue is because git-annex is trying to connect to @domain.com as the jabber
 """]]
 
 [[!tag moreinfo]]
+
+> [[done]]; git-annex removed xmpp support in 6.20170101 --[[Joey]]
diff --git a/doc/bugs/git_annex_daemon_crashes_when_authenticating_with_jabber.de.mdwn b/doc/bugs/git_annex_daemon_crashes_when_authenticating_with_jabber.de.mdwn
index cc38813557..70dbf80f2c 100644
--- a/doc/bugs/git_annex_daemon_crashes_when_authenticating_with_jabber.de.mdwn
+++ b/doc/bugs/git_annex_daemon_crashes_when_authenticating_with_jabber.de.mdwn
@@ -24,3 +24,5 @@ git-annex: interrupted
 [[!tag confirmed forwarded]]
 
 [[!meta title="OSX xmpp crash with jabber.de"]]
+
+> [[done]]; git-annex removed xmpp support in 6.20170101 --[[Joey]]

CVE-2026-112818
diff --git a/CHANGELOG b/CHANGELOG
index 21e7b5a5ee..96613b78b4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,8 @@
 git-annex (10.20261026) UNRELEASED; urgency=high
 
   * This releases fixes a security hole that allowed private data 
-    exfiltration from special remotes.
+    exfiltration from special remotes. (CVE-2026-112818)
+    https://git-annex.branchable.com/security/cross_repository_data_exfiltration_via_special_remote/
   * verifyremote: New command, used to verify the configuration of special
     remotes is safe.
   * enableremote: Display configuration and prompt for verifyremote to be
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
index 91203d5c9f..5368ecef17 100644
--- a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -1,8 +1,8 @@
-(FIXME: CVE number assignment)
-git-annex was vulnerable to a private data exfiltration attack. Joey Hess
-discovered this attack.
+CVE-2026-112818: git-annex was vulnerable to a private data exfiltration attack.
 
-This security hole was fixed in git-annex (FIXME: version)
+Joey Hess discovered this attack.
+
+This security hole was fixed in git-annex 
 
 ## attack descriptions
 
diff --git a/git-annex.cabal b/git-annex.cabal
index 9db54106ed..733b8b440d 100644
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 10.20261006
+Version: 10.20261026
 Cabal-Version: 1.12
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>

fix link to image
diff --git a/doc/install/termux.mdwn b/doc/install/termux.mdwn
index 8e30f0c48c..2990dbd2e3 100644
--- a/doc/install/termux.mdwn
+++ b/doc/install/termux.mdwn
@@ -8,7 +8,7 @@ package repository, so installation is easy:
 Just run "git-annex webapp" inside Termux.
 A browser window will open with the git-annex interface.
 
-[[!img webapp.png alt="git-annex webapp"]]
+[[!img android/webapp.png alt="git-annex webapp"]]
 
 ## Closing and reopening the webapp
 

improve attack description
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
index 1d451dc9d7..91203d5c9f 100644
--- a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -8,28 +8,40 @@ This security hole was fixed in git-annex (FIXME: version)
 
 To perform these attacks, the attacker needs to have control over one of
 the remotes of the victim's git-annex repository. For example, they may
-provide a git-annex repository that the victim clones.
+provide a git-annex repository that the victim clones. 
 
-The attacker also needs to have information about another git-annex 
-repository owned by the victim. This could be a published git repository,
-with annexed file contents that the victim is keeping private.
+The attacker also needs to have a certian amount of information about the
+victim's private repository that they have obtained in some way. For
+example, the victim may have published a git repository but be keeping the
+annexed files private.
 
 In the simplest form of the attack, the attacker provides a git-annex
-repository that the victim clones. When git-annex is initialized in the
-victim's clone, it autoenables a special remote that the attacker has
-configured to access the victim's other, private git-annex repository.
-When the victim runs a command such as `git-annex sync --content`,
-their private files are retrieved from the special remote and sent to the
-attacker's git-annex repository.
+repository that includes in its tree annexed files that the attacker knows
+are present in the victim's private git-annex repository. When git-annex is
+initialized in the victim's clone, git-annex autoenables a special remote
+that the attacker has configured to access the victim's private git-annex
+repository. When the victim runs a command such as `git-annex sync
+--content`, their private files are retrieved from the special remote and
+sent to the attacker.
 
 In another form of the attack, autoenable of special remotes is not needed.
 Instead, the attacker provides a special remote (eg a S3 bucket) along with
 their git-annex repository. The attacker waits for the victim to clone the
-repository and manually enable and use their special remote. Then they update
-the configuration of the special remote, retargeting it to access the victim's
-own special remote (eg the victim's private S3 bucket). When the victim runs a
-command such as `git-annex sync --content`, their private files are retrieved
-from their special remote and sent to the attacker's git-annex repository.
+repository and manually enable and use their special remote. Then they
+update the configuration of the special remote, retargeting it to access
+the victim's own special remote (eg the victim's private S3 bucket). When
+the victim runs a command such as `git-annex sync --content`, their private
+files are retrieved from their special remote and sent to the attacker.
+
+A third form of the attack only requires that the attacker knows the
+location of the victim's private git repository. The attacker provides a
+git-annex repository that the victim clones. When git-annex is initialized
+in the victim's clone, it autoenables a git remote pointing to the victim's
+private git repository. The first time the victim runs a command such as
+`git-annex sync --content`, the attacker learns what git-annex keys are
+used by annexed files in the private git repository, and adds them to the
+tree. The second time the victim runs such a command, their private files
+are retrieved from their private git repository and sent to the attacker.
 
 ## git-annex security fixes
 

rework remote.<name>.annex-verified
Turns out that setting a git config before enabling a type=git special
remote doesn't work, git remote add fails.
So, have to set the git config after enabling the remote. Which would
have opened up a small race condition where the config was not set,
so it appeared that the remote was verified when it was not.
To fix that, changed the value of the config (and also its name)
so when unset, it's treated as if it might be autoenabled, or might be
manually enabled.
Note that, on upgrade from old git-annex, nothing currently sets the
config. The verified remote log file gets populated though, and that's
enough, since unverifiedRemoteAmelioration only affects remotes
that are not in the verified remote log. I considered having it set
the config when populating the log, but decided it was nicer to have
this indication that the remote was set up by an old git-annex and
has not been verified.
---
Also this fixes a bug in git-annex verifyremote where it set the config
when run w/o any options.
diff --git a/CHANGELOG b/CHANGELOG
index 374e390b7e..21e7b5a5ee 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -22,7 +22,7 @@ git-annex (10.20261026) UNRELEASED; urgency=high
     them could leak out via other git-annex processes running in the
     repository.
   * init --autoenable: Refuse to perform probably unsafe action unless forced.
-  * Added remote.name.annex-unverified git config.
+  * Added remote.name.annex-verified git config.
 
  -- Joey Hess <id@joeyh.name>  Fri, 16 Oct 2026 09:29:27 -0400
 
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
index aaa33e571a..6b1788c422 100644
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -131,7 +131,7 @@ performSpecialRemote o t u name oldc c gc mcu = do
 		Just msg -> giveup $ "Cannot enable this special remote because " ++ msg
 	let reenabling = isJust prevrmt
 	let wasautoenabled = 
-		(remoteAnnexUnverified . R.gitconfig <$> prevrmt)
+		(remoteAnnexVerified . R.gitconfig <$> prevrmt)
 			== Just RemoteConfigUnverifiedAutoEnabled
 	(c', u') <- R.setup t (R.Enable oldc) (Just u) name Nothing c gc
 	next $ cleanupSpecialRemote o t u' c' mcu reenabling wasautoenabled name
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 3829a4baf2..87194cede9 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -81,7 +81,6 @@ startVerify r si = do
 	showCustom "verifyremote" si $ do
 		remoteconfig <- getUnverifiedRemoteConfig (logUUID r)
 		cid <- Logs.VerifiedRemote.storeVerifyingRemoteLog remoteconfig
-		setRemoteConfigVerification r RemoteConfigVerified
 		verifiedremoteconfig <- getVerifiedRemoteConfig (logUUID r)
 		let msg = fromMaybe nochanges $
 			remoteVerificationMessage' (Remote.name r) remoteconfig cid verifiedremoteconfig
@@ -156,6 +155,7 @@ performVerifySafe cid r = do
 		Just rc
 			| rc == Logs.VerifiedRemote.omitUnverifiableFields remoteconfig -> do
 				Logs.VerifiedRemote.configSet (logUUID r) remoteconfig
+				setRemoteConfigVerification r RemoteConfigVerified
 				Logs.VerifiedRemote.removeVerifyingLog cid
 				next $ return True
 		_ -> do
@@ -170,7 +170,7 @@ performVerifyUnsafe r = do
 		warningVerifyUnsafe
 			(Remote.name r)
 			hasverifiedconfig
-			(remoteAnnexUnverified (Remote.gitconfig r))
+			(remoteAnnexVerified (Remote.gitconfig r))
 	next $ return False
 
 warningVerifyUnsafe :: RemoteName -> Bool -> RemoteConfigVerification -> [String]
diff --git a/Config.hs b/Config.hs
index c62c079ec2..d6047f0b1e 100644
--- a/Config.hs
+++ b/Config.hs
@@ -103,7 +103,7 @@ setRemoteConfigVerification :: RemoteNameable r => r -> RemoteConfigVerification
 setRemoteConfigVerification r = 
 	maybe (unsetConfig c) (setConfig c) . formatRemoteConfigVerification
   where
-	c = remoteAnnexConfig r "unverified"
+	c = remoteAnnexConfig r "verified"
 
 isBareRepo :: Annex Bool
 isBareRepo = fromRepo Git.repoIsLocalBare
diff --git a/Logs/VerifiedRemote.hs b/Logs/VerifiedRemote.hs
index 79e950e1ad..bb8757cf08 100644
--- a/Logs/VerifiedRemote.hs
+++ b/Logs/VerifiedRemote.hs
@@ -127,7 +127,7 @@ data UnverifiedRemoteAmelioration = UnverifiedRemoteAmelioration
 
 {- Given a list of UUIDs of all currently enabled remotes, check if any of
  - them are not in the verified remote log, and depending on their
- - remote.name.annex-unverified configuration, returns the necessary
+ - remote.name.unverified configuration, returns the necessary
  - amelioration to prevent exfiltation of data from an unverified remote
  - to an attacker controlled remote.
  -}
@@ -137,9 +137,9 @@ unverifiedRemoteAmelioration
 	-- ^ Logs.Remote.remoteConfigMap
 	-> Annex (Maybe UnverifiedRemoteAmelioration)
 unverifiedRemoteAmelioration rs configmap = remoteConfigMap >>= \case
-	Just m -> return $ case mapMaybe (notinmap m) rs of
-		[] -> Nothing
-		l -> Just $ UnverifiedRemoteAmelioration
+	Just m -> return $ 
+		let l = mapMaybe (notinmap m) rs
+		in Just $ UnverifiedRemoteAmelioration
 			{ preventGitPush = 
 				checkprevented pushblocker l "Push"
 			, preventAnnexWrite =
@@ -164,20 +164,23 @@ unverifiedRemoteAmelioration rs configmap = remoteConfigMap >>= \case
 	-- Any special remote that is unverified prevents git push to all
 	-- remotes.
 	pushblocker (_, gc, remotename)
-		| remoteAnnexUnverified gc /= RemoteConfigVerified = remotename
+		| remoteAnnexVerified gc /= RemoteConfigVerified = remotename
 		| otherwise = Nothing
 
 	-- Any autoenabled special remote that is unverified prevents
 	-- annex object write to all remotes.
 	writeblocker (_, gc, remotename)
-		| remoteAnnexUnverified gc == RemoteConfigUnverifiedAutoEnabled = remotename
+		| remoteAnnexVerified gc == RemoteConfigUnverifiedAutoEnabled = remotename
+		| remoteAnnexVerified gc == RemoteConfigUnverified = remotename
 		| otherwise = Nothing
 
 	-- Prevent annex object access to any special remote that was
 	-- enabled manually but is not yet verified.
 	accessblocker l u = case filter (\(u',_, _) -> u' == u) l of
 		((_, gc, Just remotename):_)
-			| remoteAnnexUnverified gc == RemoteConfigUnverifiedManualEnabled ->
+			| remoteAnnexVerified gc == RemoteConfigUnverifiedManualEnabled ->
+				preventedunverified remotename
+			| remoteAnnexVerified gc == RemoteConfigUnverified ->
 				preventedunverified remotename
 		_ -> Nothing
 	
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
index 4e005a7995..e8ea1a42f5 100644
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -411,7 +411,7 @@ data RemoteGitConfig = RemoteGitConfig
 	, remoteAnnexPull :: Bool
 	, remoteAnnexPush :: Bool
 	, remoteAnnexReadOnly :: Bool
-	, remoteAnnexUnverified :: RemoteConfigVerification
+	, remoteAnnexVerified :: RemoteConfigVerification
 	, remoteAnnexVerify :: Bool
 	, remoteAnnexFastCopy :: Bool
 	, remoteAnnexCheckUUID :: Bool
@@ -494,8 +494,8 @@ extractRemoteGitConfig r remotename = do
 		, remoteAnnexPull = getbool PullField True
 		, remoteAnnexPush = getbool PushField True
 		, remoteAnnexReadOnly = getbool ReadOnlyField False
-		, remoteAnnexUnverified = parseRemoteConfigVerification $
-			getmaybe UnverifiedField
+		, remoteAnnexVerified = parseRemoteConfigVerification $
+			getmaybe VerifiedField
 		, remoteAnnexCheckUUID = getbool CheckUUIDField True
 		, remoteAnnexVerify = getbool VerifyField True
 		, remoteAnnexFastCopy = getbool FastCopyField False
@@ -608,7 +608,7 @@ data RemoteGitConfigField
 	| PullField
 	| PushField
 	| ReadOnlyField
-	| UnverifiedField
+	| VerifiedField
 	| CheckUUIDField
 	| VerifyField
 	| FastCopyField
@@ -683,7 +683,7 @@ remoteGitConfigField = \case
 	PullField -> inherited True "pull"
 	PushField -> inherited True "push"
 	ReadOnlyField -> inherited True "readonly"
-	UnverifiedField -> inherited True "unverified"
+	VerifiedField -> inherited True "verified"
 	CheckUUIDField -> uninherited True "checkuuid"
 	VerifyField -> inherited True "verify"
 	FastCopyField -> inherited True "fastcopy"
diff --git a/Types/RemoteConfig.hs b/Types/RemoteConfig.hs
index a34686d743..a7808f6266 100644
--- a/Types/RemoteConfig.hs
+++ b/Types/RemoteConfig.hs
@@ -79,18 +79,24 @@ data RemoteConfigVerification
 	= RemoteConfigVerified
 	| RemoteConfigUnverifiedManualEnabled
 	| RemoteConfigUnverifiedAutoEnabled
+	| RemoteConfigUnverified
+	-- ^ one or the other, but which is not yet known
 	deriving (Eq)
 
 parseRemoteConfigVerification :: Maybe String -> RemoteConfigVerification
+parseRemoteConfigVerification (Just "verified") =
+	RemoteConfigVerified
 parseRemoteConfigVerification (Just "manual") =
 	RemoteConfigUnverifiedManualEnabled
 parseRemoteConfigVerification (Just "automatic") =
 	RemoteConfigUnverifiedAutoEnabled
-parseRemoteConfigVerification _ = RemoteConfigVerified
+parseRemoteConfigVerification _ = RemoteConfigUnverified
 
 formatRemoteConfigVerification :: RemoteConfigVerification -> Maybe String
+formatRemoteConfigVerification RemoteConfigVerified =
+	Just "verified"
 formatRemoteConfigVerification RemoteConfigUnverifiedManualEnabled =
 	Just "manual"
 formatRemoteConfigVerification RemoteConfigUnverifiedAutoEnabled =
 	Just "automatic"
-formatRemoteConfigVerification RemoteConfigVerified = Nothing
+formatRemoteConfigVerification RemoteConfigUnverified = Nothing
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
index f2c7828352..4607a09d53 100644
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -1923,19 +1923,16 @@ Remotes are configured using these settings in `.git/config`.
   Can be used to tell git-annex if a remote is a bare repository

(Diff truncated)
prevent data leakage from unverified special remote via already running git-annex processes
When a git-annex process is already running, and a special remote gets
enabled but is not verified yet, there's a risk that an object will be
gotten from the special remote, and then the old process, oblivious
to the fact that there is an unverified special remote now, will send it
on to an attacker.
This problem is avoided now in different ways depending on whether the
special remote is autoenable, or enableremote was run.
When enableremote was run, it just prompted the user to verify. So it's
fine to block all access to the new special remote until it's verified.
As for autoenabled remotes, the trick is to only let the first git-annex
process run in the repository autoenable them. So there is no old
process. In order to guarantee that is the case though, running
git-annex init an an already existing repository no longer autoenables
any new remotes. And git-annex init --autoenable, which is really an
internal option, but not well documented as such needs a --force option.
In order to tell which way a special remote got enabled, there is a new
remote.<name>.annex-unverified git config. (This also lets users who really
want to aim at their feet set a git config to avoid needing to run
verifyremote.)
Note that the git config is set before the remote gets set up, to avoid
a race where a git-annex process starts at the same time and doesn't see
the config, but does see the remote. Which would be treated the same as
if the user had unset the config.
git-annex verify --unsafe now displays different messages for the
autoenable and manual cases, which lets the messages be tuned better.
Note that git push to all remotes is prevented while there is a manually
enabled unverified special remote. At first I though that wan't needed
in that case. But then I thought of an additional reason to
want to disable git push that applies to that case, as well as the
autoenabled case. If a special remote is type=git, or some other type
that git fetch works from (including using git-remote-annex), then
after a manual enable, a git fetch from it could happen. There does not
seem to be any hook that prevents git fetch. So, the best that can be
done is to prevent push to prevent anything pulled from leaking out.
This should finally wrap up dealing with this security hole.
diff --git a/Annex/Init.hs b/Annex/Init.hs
index de0d85fb10..0d1658031a 100644
--- a/Annex/Init.hs
+++ b/Annex/Init.hs
@@ -540,7 +540,7 @@ autoEnableSpecialRemotes remotelist = do
 	_ <- remotelist
 	rp <- fromRepo Git.repoPath
 	withNullHandle $ \nullh -> gitAnnexChildProcess "init"
-		[ Param "--autoenable" ]
+		[ Param "--autoenable", Param "--force" ]
 		(\p -> p
 			{ std_out = UseHandle nullh
 			, std_err = UseHandle nullh
diff --git a/Annex/SpecialRemote.hs b/Annex/SpecialRemote.hs
index 45aeb43ce7..8363a3c7a8 100644
--- a/Annex/SpecialRemote.hs
+++ b/Annex/SpecialRemote.hs
@@ -75,7 +75,8 @@ autoEnable = do
 				dummycfg <- liftIO dummyRemoteGitConfig
 				tryNonAsync (setup t (AutoEnable c) (Just u) name Nothing c dummycfg) >>= \case
 					Left e -> warning (UnquotedString (show e))
-					Right (_c, _u) ->
+					Right (_c, _u) -> do
+						setRemoteConfigVerification name Remote.RemoteConfigUnverifiedAutoEnabled
 						when (cu /= u) $
 							setRemoteConfigUUID c cu
 			_ -> return ()
diff --git a/CHANGELOG b/CHANGELOG
index 27d203e92a..374e390b7e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,31 @@
+git-annex (10.20261026) UNRELEASED; urgency=high
+
+  * This releases fixes a security hole that allowed private data 
+    exfiltration from special remotes.
+  * verifyremote: New command, used to verify the configuration of special
+    remotes is safe.
+  * enableremote: Display configuration and prompt for verifyremote to be
+    run. Until it is verified, the special remote cannot be used,
+    and git pushes to all remotes will be prevented.
+  * When a special remote has been autoenabled, allow it to be used without
+    verification, but prevent sending annexed files or git pushes to any
+    remote until it is verified.
+  * After upgrading git-annex, existing special remotes will continue to work
+    as before. If a special remote could have been configured by a third party,
+    you should verify its configuration using the `git-annex verifyremote`
+    command.
+  * Changes to special remote configuration received in the git-annex
+    branch no longer take effect until git-annex verifyremote is run to review
+    and verify the new configuration.
+  * init: When run in an already initialized repository, avoid 
+    autoenabling new special remotes, since files received from
+    them could leak out via other git-annex processes running in the
+    repository.
+  * init --autoenable: Refuse to perform probably unsafe action unless forced.
+  * Added remote.name.annex-unverified git config.
+
+ -- Joey Hess <id@joeyh.name>  Fri, 16 Oct 2026 09:29:27 -0400
+
 git-annex (10.20261007) UNRELEASED; urgency=medium
 
   * adjust --hide-unwanted: New option to make an adjusted branch
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
index b4e7e1c7c7..aaa33e571a 100644
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -25,6 +25,7 @@ import Annex.Hook
 import Config
 import Config.DynamicConfig
 import Types.GitConfig
+import Types.RemoteConfig
 import Types.ProposedAccepted
 import Command.VerifyRemote (remoteVerificationMessage)
 
@@ -128,11 +129,15 @@ performSpecialRemote o t u name oldc c gc mcu = do
 	enablePrePushHook >>= \case
 		Nothing -> noop
 		Just msg -> giveup $ "Cannot enable this special remote because " ++ msg
+	let reenabling = isJust prevrmt
+	let wasautoenabled = 
+		(remoteAnnexUnverified . R.gitconfig <$> prevrmt)
+			== Just RemoteConfigUnverifiedAutoEnabled
 	(c', u') <- R.setup t (R.Enable oldc) (Just u) name Nothing c gc
-	next $ cleanupSpecialRemote o t u' c' mcu prevrmt name
+	next $ cleanupSpecialRemote o t u' c' mcu reenabling wasautoenabled name
 
-cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> Maybe Remote -> RemoteName -> CommandCleanup
-cleanupSpecialRemote o t u c mcu prevrmt name = do
+cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> Bool -> Bool -> RemoteName -> CommandCleanup
+cleanupSpecialRemote o t u c mcu reenabling wasautoenabled name = do
 	cu <- case mcu of
 		Nothing -> do
 			Logs.Remote.configSet u c
@@ -141,8 +146,10 @@ cleanupSpecialRemote o t u c mcu prevrmt name = do
 			setRemoteConfigUUID c cu
 			Logs.Remote.configSet cu c
 			return cu
-	when (isJust prevrmt) $
+	when reenabling $
 		Logs.VerifiedRemote.configUpdate cu c
+	unless (reenabling && not wasautoenabled) $
+		setRemoteConfigVerification name RemoteConfigUnverifiedManualEnabled
 	remoteVerificationMessage name cu c >>= \case
 		Just msg -> showLongNote (UnquotedString msg)
 		Nothing -> noop
diff --git a/Command/Init.hs b/Command/Init.hs
index 32b14665b7..0d3c5f19cd 100644
--- a/Command/Init.hs
+++ b/Command/Init.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -14,6 +14,7 @@ import Annex.Init
 import Annex.Version
 import Annex.Startup
 import Types.RepoVersion
+import qualified Annex
 import qualified Annex.SpecialRemote
 
 import Control.Monad.Fail as Fail (MonadFail(..))
@@ -72,20 +73,23 @@ start os
 
 perform :: InitOptions -> CommandPerform
 perform os = do
+	startversion <- getVersion
 	case initVersion os of
 		Nothing -> noop
-		Just wantversion -> getVersion >>= \case
+		Just wantversion -> case startversion of
 			Just v | v /= wantversion ->
 				giveup $ "This repository is already a initialized with version " ++ show (fromRepoVersion v) ++ ", not changing to requested version."
 			_ -> noop
 	initialize startupAnnex
 		(if null (initDesc os) then Nothing else Just (initDesc os))
 		(initVersion os)
-	unless (noAutoEnable os)
+	unless (noAutoEnable os || isJust startversion) $
 		Annex.SpecialRemote.autoEnable
 	next $ return True
 
 performAutoEnableOnly :: CommandPerform
 performAutoEnableOnly = do
+	unlessM (Annex.getRead Annex.force) $
+		giveup "It is not safe to autoenable new special remotes if any other git-annex processes may be running. Pass --force if you are sure you want to do this."
 	Annex.SpecialRemote.autoEnable
 	next $ return True
diff --git a/Command/InitRemote.hs b/Command/InitRemote.hs
index 832eef36a0..e7aef06389 100644
--- a/Command/InitRemote.hs
+++ b/Command/InitRemote.hs
@@ -136,6 +136,7 @@ cleanup t u name c o = do
 			setRemoteConfigUUID c cu
 			Logs.Remote.configSet cu c
 			Logs.VerifiedRemote.configSet cu c
+	setRemoteConfigVerification name RemoteConfigVerified
 	when (withUrl o) $
 		setAnnexUrl c
 	unless (Remote.gitSyncableRemoteType t || withUrl o) $
diff --git a/Command/PrePush.hs b/Command/PrePush.hs
index 13e1e27646..347dd0c13a 100644
--- a/Command/PrePush.hs
+++ b/Command/PrePush.hs
@@ -11,7 +11,7 @@ import Command
 import Remote.List
 import Types.Remote
 import qualified Logs.Remote
-import qualified Logs.VerifiedRemote
+import Logs.VerifiedRemote
 
 cmd :: Command
 cmd = noCommit $ command "pre-push" SectionPlumbing
@@ -23,5 +23,8 @@ seek :: CmdParams -> CommandSeek
 seek _ = do
 	rs <- map (\r -> (uuid r, gitconfig r, Just (name r))) <$> remoteList
 	m <- Logs.Remote.remoteConfigMap
-	maybe noop giveup
-		=<< Logs.VerifiedRemote.checkNeedReadOnly rs m "Push"
+	unverifiedRemoteAmelioration rs m >>= \case
+		Nothing -> noop
+		Just amelioration ->
+			maybe noop giveup $
+				preventGitPush amelioration
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index f07e39b9b1..3829a4baf2 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -17,6 +17,7 @@ import qualified Remote
 import qualified Types.Remote as Remote
 import Types.RemoteConfig
 import Types.ProposedAccepted
+import Config
 import Annex.SpecialRemote.Config
 
 import qualified Data.Map as M

(Diff truncated)
git-annex pre-push
diff --git a/Annex/Hook.hs b/Annex/Hook.hs
index 366678490c..20abc24e38 100644
--- a/Annex/Hook.hs
+++ b/Annex/Hook.hs
@@ -4,7 +4,7 @@
  - git-annex not change, otherwise removing old hooks using an old
  - version of the script would fail.
  -
- - Copyright 2013-2025 Joey Hess <id@joeyh.name>
+ - Copyright 2013-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -26,14 +26,27 @@ preCommitHook = Git.Hook (literalOsPath "pre-commit")
 
 postReceiveHook :: Git.Hook
 postReceiveHook = Git.Hook (literalOsPath "post-receive")
-	-- Only run git-annex post-receive when git-annex supports it,
-	-- to avoid failing if the repository with this hook is used
-	-- with an older version of git-annex.
-	(mkHookScript "if git annex post-receive --help >/dev/null 2>&1; then git annex post-receive; fi")
+	(mkHookScript (runHookCommandWhenSupported "post-receive"))
 	-- This is an old version of the hook script.
 	[ mkHookScript "git annex post-receive"
 	]
 
+-- Only run a git-annex hook command when git-annex supports it, to avoid
+-- failing if the repository with a hook is used with an older version of
+-- git-annex.
+runHookCommandWhenSupported :: String -> String
+runHookCommandWhenSupported command = concat
+	[ "if git annex "
+	, command
+	, " --help >/dev/null 2>&1; then git annex "
+	, command
+	, "; fi"
+	]
+
+prePushHook :: Git.Hook
+prePushHook = Git.Hook (literalOsPath "pre-push")
+	(mkHookScript (runHookCommandWhenSupported "pre-push")) []
+
 postCheckoutHook :: Git.Hook
 postCheckoutHook = Git.Hook (literalOsPath "post-checkout") smudgeHook []
 
@@ -80,16 +93,21 @@ hookWrite :: Git.Hook -> Annex ()
 hookWrite h = unlessM (inRepo $ Git.hookWrite h) $
 	hookWarning h "already exists, not configuring"
 
+tryHookWrite :: Git.Hook -> Annex (Either SomeException Bool)
+tryHookWrite h = tryNonAsync (inRepo $ Git.hookWrite h)
+
 hookUnWrite :: Git.Hook -> Annex ()
 hookUnWrite h = unlessM (inRepo $ Git.hookUnWrite h) $
 	hookWarning h "contents modified; not deleting. Edit it to remove call to git annex."
 
 hookWarning :: Git.Hook -> String -> Annex ()
-hookWarning h msg = do
+hookWarning h msg = warning . UnquotedString =<< hookMessage h msg
+
+hookMessage :: Git.Hook -> String -> Annex String
+hookMessage h msg = do
 	r <- gitRepo
-	warning $ UnquotedString $
-		fromOsPath (Git.hookName h) ++ 
-			" hook (" ++ fromOsPath (Git.hookFile h r) ++ ") " ++ msg
+	return $ fromOsPath (Git.hookName h) ++ 
+		" hook (" ++ fromOsPath (Git.hookFile h r) ++ ") " ++ msg
 
 {- To avoid checking if the hook exists every time, the existing hooks
  - are cached. -}
@@ -170,3 +188,11 @@ outputOfAnnexHook hook commandcfg = ifM (doesAnnexHookExist hook)
 		Nothing -> return Nothing
 		Just command -> liftIO $ 
 			Just <$> readProcess "sh" ["-c", command]
+
+enablePrePushHook :: Annex (Maybe String)
+enablePrePushHook = tryHookWrite prePushHook >>= \case
+	Right True -> return Nothing
+	Right False -> Just <$> hookMessage prePushHook
+		"is not configured by git-annex."
+	Left _ex -> Just <$> hookMessage prePushHook
+		"does not exist and unable to write it."
diff --git a/Annex/Init.hs b/Annex/Init.hs
index 9bdca94d82..de0d85fb10 100644
--- a/Annex/Init.hs
+++ b/Annex/Init.hs
@@ -165,6 +165,7 @@ initialize' startupannex mversion _initallowed = do
 	unlessM isBareRepo $ do
 		hookWrite postCheckoutHook
 		hookWrite postMergeHook
+	hookWrite prePushHook
 
 	AdjustedBranch.checkAdjustedClone >>= \case
 		AdjustedBranch.InAdjustedClone -> return ()
@@ -193,6 +194,7 @@ uninitialize = do
 	hookUnWrite postReceiveHook
 	hookUnWrite postCheckoutHook
 	hookUnWrite postMergeHook
+	hookUnWrite prePushHook
 	deconfigureSmudgeFilter
 	removeRepoUUID
 	removeVersion
diff --git a/Annex/SpecialRemote.hs b/Annex/SpecialRemote.hs
index ca4ee447bf..45aeb43ce7 100644
--- a/Annex/SpecialRemote.hs
+++ b/Annex/SpecialRemote.hs
@@ -14,6 +14,7 @@ module Annex.SpecialRemote (
 
 import Annex.Common
 import Annex.SpecialRemote.Config
+import Annex.Hook
 import Types.Remote (RemoteConfig, SetupStage(..), setup)
 import Types.GitConfig
 import Types.ProposedAccepted
@@ -94,10 +95,14 @@ autoEnable = do
 		| otherwise = do
 			rs <- remoteList' False
 			case filter (\rmt -> Remote.name rmt == name) rs of
-				(rmt:_) | Remote.uuid rmt /= u -> warning $ 
-					UnquotedString $ "Cannot auto enable special remote " 
-						++ name ++ " because there is another remote with the same name."
-				_ -> cont
+				(rmt:_) | Remote.uuid rmt /= u -> cannotenable name 
+						"there is another remote with the same name."
+				_ -> enablePrePushHook >>= \case
+					Nothing -> cont
+					Just msg -> warning $ UnquotedString msg
+	cannotenable name msg = warning $
+		UnquotedString $ "Cannot auto enable special remote "
+			++ name ++ " because " ++ msg
 
 autoEnableable :: Annex (M.Map UUID RemoteConfig)
 autoEnableable = do
diff --git a/CmdLine/GitAnnex.hs b/CmdLine/GitAnnex.hs
index 9a0bf41a22..fbb51ea79e 100644
--- a/CmdLine/GitAnnex.hs
+++ b/CmdLine/GitAnnex.hs
@@ -70,6 +70,7 @@ import qualified Command.AddUnused
 import qualified Command.Unlock
 import qualified Command.Lock
 import qualified Command.PreCommit
+import qualified Command.PrePush
 import qualified Command.PostReceive
 import qualified Command.FilterBranch
 import qualified Command.Find
@@ -186,6 +187,7 @@ cmds testoptparser testrunner mkbenchmarkgenerator = map addGitAnnexCommonOption
 	, Command.Uninit.cmd
 	, Command.Reinit.cmd
 	, Command.PreCommit.cmd
+	, Command.PrePush.cmd
 	, Command.PostReceive.cmd
 	, Command.NumCopies.cmd
 	, Command.MinCopies.cmd
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
index 67cf5c82f9..b4e7e1c7c7 100644
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2013-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2013-2026 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -21,6 +21,7 @@ import qualified Remote.Git
 import qualified Command.InitRemote
 import Logs.UUID
 import Annex.UUID
+import Annex.Hook
 import Config
 import Config.DynamicConfig
 import Types.GitConfig
@@ -124,6 +125,9 @@ performSpecialRemote o t u name oldc c gc mcu = do
 		_ -> return Nothing
 	unless (maybe u Remote.uuid prevrmt == u) $
 		giveup $ "Not overwriting currently configured git remote named \"" ++ name ++ "\""
+	enablePrePushHook >>= \case
+		Nothing -> noop
+		Just msg -> giveup $ "Cannot enable this special remote because " ++ msg
 	(c', u') <- R.setup t (R.Enable oldc) (Just u) name Nothing c gc
 	next $ cleanupSpecialRemote o t u' c' mcu prevrmt name
 
diff --git a/Command/PrePush.hs b/Command/PrePush.hs
new file mode 100644
index 0000000000..13e1e27646
--- /dev/null
+++ b/Command/PrePush.hs
@@ -0,0 +1,27 @@
+{- git-annex command
+ -
+ - Copyright 2026 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+module Command.PrePush where
+
+import Command

(Diff truncated)
decided not to bother the user about verifying changes to special remote configs
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index fffa0d6514..0185d55735 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -57,10 +57,3 @@ that any fix to the main attack will also close off this one.)
   remotes when there are other git-annex processes running.
 * Disable all git pushes (using git pre-push hook), to avoid
   presence information leaking out.
-* when using a remote and remote.log has a changed RemoteConfig,
-  warn user that the configuration has changed and suggest running
-  git-annex verifyremote. How to avoid warning too often or not often
-  enough? Or, maybe this is not needed, and the user can decide
-  on their own when they want to verify the new configuration?  
-  Leaning toward letting the user decide. Now that updates are not needed
-  for encryption changes.

simpler ids for verifyremote
Needs a whole separate log, but the UI improvement is worth it.
This also avoids any potential SHA2 collision attack risk.
diff --git a/Annex/Locations.hs b/Annex/Locations.hs
index 87602918ca..d032f5ed36 100644
--- a/Annex/Locations.hs
+++ b/Annex/Locations.hs
@@ -63,6 +63,8 @@ module Annex.Locations (
 	gitAnnexRestageLock,
 	gitAnnexVerifiedRemoteLog,
 	gitAnnexVerifiedRemoteLock,
+	gitAnnexVerifyingRemoteLog,
+	gitAnnexVerifyingRemoteLock,
 	gitAnnexAdjustedBranchUpdateLog,
 	gitAnnexAdjustedBranchUpdateLock,
 	gitAnnexMigrateLog,
@@ -498,6 +500,14 @@ gitAnnexVerifiedRemoteLog r = gitAnnexDir r </> literalOsPath "verified-remote.l
 gitAnnexVerifiedRemoteLock :: Git.Repo -> OsPath
 gitAnnexVerifiedRemoteLock r = gitAnnexDir r </> literalOsPath "verified-remote.lck"
 
+{- git/annex/verifying-remote.log is used by git-annex verify to record
+ - the config that a user is in the process of verifying. -}
+gitAnnexVerifyingRemoteLog :: Git.Repo -> OsPath
+gitAnnexVerifyingRemoteLog r = gitAnnexDir r </> literalOsPath "verifying-remote.log"
+
+gitAnnexVerifyingRemoteLock :: Git.Repo -> OsPath
+gitAnnexVerifyingRemoteLock r = gitAnnexDir r </> literalOsPath "verifying-remote.lck"
+
 {- .git/annex/adjust.log is used to log when the adjusted branch needs to
  - be updated. -}
 gitAnnexAdjustedBranchUpdateLog :: Git.Repo -> OsPath
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 1018b6d673..a8b1ea9c5b 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -12,12 +12,12 @@ module Command.VerifyRemote where
 import Command
 import qualified Logs.Remote
 import qualified Logs.VerifiedRemote
+import Logs.VerifiedRemote (VerifyingRemoteConfigId(..))
 import qualified Remote
 import qualified Types.Remote as Remote
 import Types.RemoteConfig
 import Types.ProposedAccepted
 import Annex.SpecialRemote.Config
-import Utility.Hash
 
 import qualified Data.Map as M
 import qualified Data.Set as S
@@ -33,10 +33,7 @@ data VerifyRemoteOptions = VerifyRemoteOptions
 	, safeunsafe :: Maybe SafeUnsafe
 	}
 
-data SafeUnsafe = Safe RemoteConfigId | Unsafe
-
-data RemoteConfigId = RemoteConfigId { fromRemoteConfigId :: String }
-	deriving (Eq)
+data SafeUnsafe = Safe VerifyingRemoteConfigId | Unsafe
 
 optParser :: CmdParamsDesc -> Parser VerifyRemoteOptions
 optParser _ = VerifyRemoteOptions
@@ -52,7 +49,7 @@ optParser _ = VerifyRemoteOptions
 safeUnsafeParser :: Parser SafeUnsafe
 safeUnsafeParser = safe <|> unsafe
   where
-	safe = option (Safe . RemoteConfigId <$> str)
+	safe = option (Safe . VerifyingRemoteConfigId <$> maybeReader readish)
 		( long "safe" <> metavar "ID"
 		<> help "special remote configuration is safe"
 		)
@@ -82,9 +79,10 @@ startVerify :: Remote -> SeekInput -> CommandStart
 startVerify r si = do
 	showCustom "verifyremote" si $ do
 		remoteconfig <- getUnverifiedRemoteConfig (logUUID r)
+		cid <- Logs.VerifiedRemote.storeVerifyingRemoteLog remoteconfig
 		verifiedremoteconfig <- getVerifiedRemoteConfig (logUUID r)
 		let msg = fromMaybe nochanges $
-			remoteVerificationMessage' (Remote.name r) remoteconfig verifiedremoteconfig
+			remoteVerificationMessage' (Remote.name r) remoteconfig cid verifiedremoteconfig
 		showRaw (encodeBS msg)
 		return True
 	stop
@@ -94,10 +92,11 @@ startVerify r si = do
 remoteVerificationMessage :: RemoteName -> UUID -> RemoteConfig -> Annex (Maybe String)
 remoteVerificationMessage remotename u remoteconfig = 
 	remoteVerificationMessage' remotename remoteconfig
-		<$> getVerifiedRemoteConfig u
+		<$> Logs.VerifiedRemote.storeVerifyingRemoteLog remoteconfig
+		<*> getVerifiedRemoteConfig u
 
-remoteVerificationMessage' :: RemoteName -> RemoteConfig -> RemoteConfig -> Maybe String
-remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
+remoteVerificationMessage' :: RemoteName -> RemoteConfig -> VerifyingRemoteConfigId -> RemoteConfig -> Maybe String
+remoteVerificationMessage' remotename remoteconfig cid verifiedremoteconfig
 	| visiblefields verifiedremoteconfig == visibleremoteconfig = Nothing
 	| otherwise = Just $
 		if verifiedremoteconfig /= mempty
@@ -128,12 +127,10 @@ remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
 
 	verifysafecommand = "git-annex verifyremote "
 		 ++ remotename ++ " --safe "
-		 ++ fromRemoteConfigId cid
+		 ++ show (fromVerifyingRemoteConfigId cid)
 
 	verifyunsafecommand = "git-annex verifyremote "
 		++ remotename ++ " --unsafe"
-
-	cid = genRemoteConfigId remoteconfig
 	
 	visibleremoteconfig = visiblefields remoteconfig
 	
@@ -147,14 +144,17 @@ remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
 		, sameasUUIDField
 		]
 
-performVerifySafe :: RemoteConfigId -> Remote -> CommandPerform
+performVerifySafe :: VerifyingRemoteConfigId -> Remote -> CommandPerform
 performVerifySafe cid r = do
 	remoteconfig <- getUnverifiedRemoteConfig (logUUID r)
-	if genRemoteConfigId remoteconfig == cid
-		then do
-			Logs.VerifiedRemote.configSet (logUUID r) remoteconfig
-			next $ return True
-		else do
+	m <- Logs.VerifiedRemote.readVerifyingRemoteLog
+	case M.lookup cid m of
+		Just rc
+			| rc == Logs.VerifiedRemote.omitUnverifiableFields remoteconfig -> do
+				Logs.VerifiedRemote.configSet (logUUID r) remoteconfig
+				Logs.VerifiedRemote.removeVerifyingLog cid
+				next $ return True
+		_ -> do
 			warning "invalid ID"
 			next $ return False
 
@@ -218,25 +218,6 @@ logUUID :: Remote -> UUID
 logUUID r = fromMaybe (Remote.uuid r) $
 	remoteAnnexConfigUUID (Remote.gitconfig r)
 
-{- The RemoteConfig is hashed to generate the RemoteConfigId. This
- - ensures that the config does not change between the verification
- - display and marking the config as safe.
- -}
-genRemoteConfigId :: RemoteConfig -> RemoteConfigId
-genRemoteConfigId = RemoteConfigId
-	. decodeBS
-	. hashByteString
-	. digestToHash
-	. sha2_224 
-	. encodeBL
-	. concat
-	. concatMap go
-	. sort
-	. M.toList
-	. Logs.VerifiedRemote.omitUnverifiableFields
-  where
-	go (f, v) = [fromProposedAccepted f, fromProposedAccepted v]
-
 displayRemoteConfig :: RemoteConfig -> String
 displayRemoteConfig = intercalate " " . map display . sortBy order . M.toList
   where
diff --git a/Logs/Remote/Pure.hs b/Logs/Remote/Pure.hs
index d48c6529d8..3437ded365 100644
--- a/Logs/Remote/Pure.hs
+++ b/Logs/Remote/Pure.hs
@@ -10,8 +10,10 @@ module Logs.Remote.Pure (
 	calcRemoteConfigMapInherited,
 	parseRemoteConfigLog,
 	buildRemoteConfigLog,
+	remoteConfigParser,
         keyValToConfig,
         configToKeyVal,
+	buildRemoteConfig,
         showConfig,
 
         prop_isomorphic_configEscape,
@@ -42,11 +44,14 @@ parseRemoteConfigLog :: L.ByteString -> Log RemoteConfig
 parseRemoteConfigLog = parseLogOld remoteConfigParser
 
 buildRemoteConfigLog :: Log RemoteConfig -> Builder
-buildRemoteConfigLog = buildLogOld (byteString . encodeBS . showConfig)
+buildRemoteConfigLog = buildLogOld buildRemoteConfig
 
 remoteConfigParser :: A.Parser RemoteConfig
 remoteConfigParser = keyValToConfig Accepted . words . decodeBS <$> A.takeByteString
 
+buildRemoteConfig :: RemoteConfig -> Builder
+buildRemoteConfig = byteString . encodeBS . showConfig
+
 showConfig :: RemoteConfig -> String
 showConfig = unwords . configToKeyVal
 
diff --git a/Logs/VerifiedRemote.hs b/Logs/VerifiedRemote.hs
index c74556c8b8..a6039ca137 100644
--- a/Logs/VerifiedRemote.hs
+++ b/Logs/VerifiedRemote.hs
@@ -18,11 +18,16 @@ module Logs.VerifiedRemote (
 	checkNeedReadOnly,
 	omitUnverifiableFields,
 	addUnverifiableFields,
+	VerifyingRemoteConfigId(..),
+	readVerifyingRemoteLog,
+	storeVerifyingRemoteLog,

(Diff truncated)
checklist updates
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index a391e10987..c2632d04c4 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -50,22 +50,9 @@ that any fix to the main attack will also close off this one.)
 
 ## fix checklist
 
-* implement verified remote log, and make RemoteConfig values from it be
-  preferred over git-annex branch remote.log (done)
-* initremote populate verified remote log (done)
-* implement git-annex verifyremote (done)
-* enableremote of a new remote should display remote configuration and
-  tell user to check it and run git-annex verifyremote (done)
-* enableremote of an existing remote should modify the (verified)
-  remote configuration as specified by the user, and save it as
-  the new verified configuration, and to the git-annex branch
-  (done)
-* handle upgrades (done)
-* When any enabled special remote does not have an entry in the verified
-  remote log, disable annex objects transfers to all remotes, to avoid
-  annex objects leaking out of it to other remotes. (done)
 * What to do about already running processes that could send annex objects
-  that get received from the special remote on to other remotes?
+  that get received from a just-enabled special remote on to other
+  remotes?
   Perhaps this would best be avoided by not allowing enabling special
   remotes when there are other git-annex processes running.
 * Disable all git pushes (using git pre-push hook), to avoid
@@ -74,7 +61,9 @@ that any fix to the main attack will also close off this one.)
   warn user that the configuration has changed and suggest running
   git-annex verifyremote. How to avoid warning too often or not often
   enough? Or, maybe this is not needed, and the user can decide
-  on their own when they want to verify the new configuration?
+  on their own when they want to verify the new configuration?  
+  Leaning toward letting the user decide. Now that updates are not needed
+  for encryption changes.
 * Maybe make verifyremote store the config that was shown to the user,
   avoiding the need to copy and paste the config hash.
   But, bear in mind that the current design is safe even if git-annex

enableremote run twice in a row won't verifiy an unverified remote
If the remote is already verified, then the changes made by enableremote
get put into the verified remote log.
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
index 2466517396..67cf5c82f9 100644
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -125,20 +125,23 @@ performSpecialRemote o t u name oldc c gc mcu = do
 	unless (maybe u Remote.uuid prevrmt == u) $
 		giveup $ "Not overwriting currently configured git remote named \"" ++ name ++ "\""
 	(c', u') <- R.setup t (R.Enable oldc) (Just u) name Nothing c gc
-	if prevrmt == Nothing
-		then remoteVerificationMessage name u c' >>= \case
-			Just msg -> showLongNote (UnquotedString msg)
-			Nothing -> noop
-		else Logs.VerifiedRemote.configSet u' c 
-	next $ cleanupSpecialRemote o t u' c' mcu
-
-cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> CommandCleanup
-cleanupSpecialRemote o t u c mcu = do
-	case mcu of
-		Nothing -> Logs.Remote.configSet u c
+	next $ cleanupSpecialRemote o t u' c' mcu prevrmt name
+
+cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> Maybe Remote -> RemoteName -> CommandCleanup
+cleanupSpecialRemote o t u c mcu prevrmt name = do
+	cu <- case mcu of
+		Nothing -> do
+			Logs.Remote.configSet u c
+			return u
 		Just (SpecialRemote.ConfigFrom cu) -> do
 			setRemoteConfigUUID c cu
 			Logs.Remote.configSet cu c
+			return cu
+	when (isJust prevrmt) $
+		Logs.VerifiedRemote.configUpdate cu c
+	remoteVerificationMessage name cu c >>= \case
+		Just msg -> showLongNote (UnquotedString msg)
+		Nothing -> noop
 	Remote.byUUID u >>= \case
 		Nothing -> noop
 		Just r -> do
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 239aa73699..1018b6d673 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -233,6 +233,7 @@ genRemoteConfigId = RemoteConfigId
 	. concatMap go
 	. sort
 	. M.toList
+	. Logs.VerifiedRemote.omitUnverifiableFields
   where
 	go (f, v) = [fromProposedAccepted f, fromProposedAccepted v]
 
diff --git a/Logs/VerifiedRemote.hs b/Logs/VerifiedRemote.hs
index 62e2d97364..c74556c8b8 100644
--- a/Logs/VerifiedRemote.hs
+++ b/Logs/VerifiedRemote.hs
@@ -14,6 +14,7 @@ module Logs.VerifiedRemote (
 	remoteConfigMap,
 	readRemoteLog,
 	configSet,
+	configUpdate,
 	checkNeedReadOnly,
 	omitUnverifiableFields,
 	addUnverifiableFields,
@@ -25,6 +26,7 @@ import Types.Remote
 import Logs.File
 import Logs.UUIDBased
 import Logs.Remote.Pure
+import Logs.MapLog
 import Annex.SpecialRemote.Config
 import qualified Utility.FileIO as F
 
@@ -32,10 +34,21 @@ import qualified Data.Map as M
 import Data.ByteString.Builder
 
 configSet :: UUID -> RemoteConfig -> Annex ()
-configSet u c = configSet' [(u, c)]
+configSet u c = configSetMulti [(u, c)]
 
-configSet' :: [(UUID, RemoteConfig)] -> Annex ()
-configSet' ucfgs = do
+{- Only sets the RemoteConfig if the log already contains a RemoteConfig
+ - for this UUID. -}
+configUpdate :: UUID -> RemoteConfig -> Annex ()
+configUpdate u c = configSetMulti' (M.member u . fromMapLog) [(u, c)]
+
+configSetMulti :: [(UUID, RemoteConfig)] -> Annex ()
+configSetMulti = configSetMulti' (const True)
+
+configSetMulti'
+	:: (Log RemoteConfig -> Bool)
+	-> [(UUID, RemoteConfig)]
+	-> Annex ()
+configSetMulti' shouldset ucfgs = do
 	c <- currentVectorClock
 	logf <- fromRepo gitAnnexVerifiedRemoteLog
 	lck <- fromRepo gitAnnexVerifiedRemoteLock
@@ -47,8 +60,10 @@ configSet' ucfgs = do
 	Annex.changeState $ \s ->
 		s { Annex.verifiedremoteconfigmap = Nothing }
   where
-	accumchange c v (u, cfg) = 
-		changeLog c u (omitUnverifiableFields cfg) v
+	accumchange c old (u, cfg)
+		| shouldset old =
+			changeLog c u (omitUnverifiableFields cfg) old
+		| otherwise = old
 
 {- Hidden encryption fields cannot be verified by the user, and in any
  - case do not configure the location that the remote accesses, and so
@@ -126,7 +141,7 @@ checkNeedReadOnly rs configmap = remoteConfigMap >>= \case
 	-- In a newly initialized repository, there are not yet
 	-- any enabled remotes, and this creates an empty log file.
 	Nothing -> do
-		configSet' $ mapMaybe (copyfromconfigmap configmap) rs
+		configSetMulti $ mapMaybe (copyfromconfigmap configmap) rs
 		return Nothing
   where
 	notinmap m (u, gc, remotename)
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index 0f64345a83..a391e10987 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -83,5 +83,3 @@ that any fix to the main attack will also close off this one.)
   The hash guarantees that what gets verified is what the user saw.
   What should be possible is to have a simple counter, rather than a hash,
   with a log of verification prompts that the counter indexes into.
-* Running git-annex enableremote twice in a row verifies it the second
-  time. Shouldn't!

avoid storing encryption fields in verified remote log
Those fields cannot be verified, and so it makes sense for them to get
updated whenever the git-annex remote.log changes.
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 34b404b825..239aa73699 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -83,18 +83,13 @@ startVerify r si = do
 	showCustom "verifyremote" si $ do
 		remoteconfig <- getUnverifiedRemoteConfig (logUUID r)
 		verifiedremoteconfig <- getVerifiedRemoteConfig (logUUID r)
-		msg <- case remoteVerificationMessage' (Remote.name r) remoteconfig verifiedremoteconfig of
-			Just msg -> return msg
-			Nothing -> if verifiedremoteconfig == remoteconfig
-				then return "There have been no changes to the remote's configuration since it was last verified."
-				else do
-					-- Store new config as verified so that any
-					-- changes to the omitfields take effect.
-					Logs.VerifiedRemote.configSet (logUUID r) remoteconfig
-					return "No verification is necessary; updated the remote's configuration."			
+		let msg = fromMaybe nochanges $
+			remoteVerificationMessage' (Remote.name r) remoteconfig verifiedremoteconfig
 		showRaw (encodeBS msg)
 		return True
 	stop
+  where
+	nochanges = "There have been no changes to the remote's configuration since it was last verified."
 
 remoteVerificationMessage :: RemoteName -> UUID -> RemoteConfig -> Annex (Maybe String)
 remoteVerificationMessage remotename u remoteconfig = 
@@ -141,10 +136,12 @@ remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
 	cid = genRemoteConfigId remoteconfig
 	
 	visibleremoteconfig = visiblefields remoteconfig
-
-	visiblefields = flip M.withoutKeys omitfields
-
-	omitfields = hiddenEncryptionFields <> S.fromList
+	
+	visiblefields = Logs.VerifiedRemote.omitUnverifiableFields
+		. flip M.withoutKeys omitfields
+	
+	-- Fields that do not need to be verified.
+	omitfields = S.fromList
 		[ nameField
 		, sameasNameField
 		, sameasUUIDField
diff --git a/Logs/Remote.hs b/Logs/Remote.hs
index fa967562dd..22bec2a804 100644
--- a/Logs/Remote.hs
+++ b/Logs/Remote.hs
@@ -43,15 +43,19 @@ configSet u cfg = do
 {- Map of remotes by uuid with their RemoteConfigs. Cached for speed.
  -
  - When the verified remote log has a RemoteConfig, it is used, otherwise
- - the value from the git-annex branch is used. This ensures that, once a
- - user has verified a RemoteConfig, it does not change unexpectedly.
+ - the value from the git-annex branch log is used. This ensures that, once
+ - a user has verified a RemoteConfig, it does not change unexpectedly.
+ - 
+ - Fields that are not verifiable are always taken from the git-annex
+ - branch log.
  -}
 remoteConfigMap :: Annex (M.Map UUID RemoteConfig)
 remoteConfigMap = maybe load return =<< Annex.getState Annex.remoteconfigmap
   where
 	load = do
 		logged <- readRemoteLog
-		verified <- fromMaybe mempty
+		verified <- Logs.VerifiedRemote.addUnverifiableFields logged
+			. fromMaybe mempty
 			<$> Logs.VerifiedRemote.remoteConfigMap
 		let m = M.union verified logged
 		Annex.changeState $ \s ->
diff --git a/Logs/VerifiedRemote.hs b/Logs/VerifiedRemote.hs
index 7c23d93d1f..62e2d97364 100644
--- a/Logs/VerifiedRemote.hs
+++ b/Logs/VerifiedRemote.hs
@@ -1,8 +1,7 @@
 {- git-annex verified remote log
  - 
- - This contains the same information as the git-annex remote log,
- - but only remote configs that have been verified by the user to be safe
- - to use.
+ - This is similar to the git-annex remote log, but only remote configs
+ - that have been verified by the user to be safe to use.
  -
  - Copyright 2026 Joey Hess <id@joeyh.name>
  - 
@@ -16,6 +15,8 @@ module Logs.VerifiedRemote (
 	readRemoteLog,
 	configSet,
 	checkNeedReadOnly,
+	omitUnverifiableFields,
+	addUnverifiableFields,
 ) where
 
 import Annex.Common
@@ -42,8 +43,34 @@ configSet' ucfgs = do
 		<$> readLogFile logf lck
 	writeLogFile' logf $ \tmp -> F.writeFile tmp $
 		toLazyByteString $ buildRemoteConfigLog $
-			foldl' (\v (u, cfg) -> changeLog c u cfg v) old ucfgs
-	Annex.changeState $ \s -> s { Annex.verifiedremoteconfigmap = Nothing }
+			foldl' (accumchange c) old ucfgs
+	Annex.changeState $ \s ->
+		s { Annex.verifiedremoteconfigmap = Nothing }
+  where
+	accumchange c v (u, cfg) = 
+		changeLog c u (omitUnverifiableFields cfg) v
+
+{- Hidden encryption fields cannot be verified by the user, and in any
+ - case do not configure the location that the remote accesses, and so
+ - are omitted from this log file.
+ -}
+omitUnverifiableFields :: RemoteConfig -> RemoteConfig
+omitUnverifiableFields c = M.withoutKeys c hiddenEncryptionFields
+
+addUnverifiableFields
+	:: M.Map UUID RemoteConfig
+	-> M.Map UUID RemoteConfig
+	-> M.Map UUID RemoteConfig
+addUnverifiableFields unverifiedconfigs verifiedconfigs =
+	M.mapWithKey go verifiedconfigs
+  where
+	go u verifiedconfig = case M.lookup u unverifiedconfigs of
+		Just unverifiedconfig ->
+			let unverifiable = M.restrictKeys
+				unverifiedconfig
+				hiddenEncryptionFields
+			in M.union unverifiable verifiedconfig
+		Nothing -> verifiedconfig
 
 {- Map of remotes by uuid with their RemoteConfigs. Cached for speed.
  -
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index 0b70077c9a..0f64345a83 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -83,10 +83,5 @@ that any fix to the main attack will also close off this one.)
   The hash guarantees that what gets verified is what the user saw.
   What should be possible is to have a simple counter, rather than a hash,
   with a log of verification prompts that the counter indexes into.
-* Think about how to handle an update to a remote's encryption.
-  It might make sense to copy the hiddenEncryptionFields from the
-  git-annex branch remote.log even then otherwise using a verified
-  RemoteConfig. These fields can't be used to exploit this security hole,
-  and needing to run git-annex verifyremote to update them is weird,
-  and seems to have no security benefit since the user doesn't get a chance
-  to review them anyway.
+* Running git-annex enableremote twice in a row verifies it the second
+  time. Shouldn't!

display config in conventional order
This makes it easier to read, which improves how effective the user's
verification is.
Could have made RemoteConfigField a newtype and made this its Ord
instance. Partly I didn't because it would make the security patch
signifiantly larger. Also, the existing ordering is used in remote.log,
and changing it would make minor config changes reorder in the updated
log, slightly bloating the size of the git-annex branch.
diff --git a/Annex/SpecialRemote/Config.hs b/Annex/SpecialRemote/Config.hs
index f9a03bbdcd..341cd1050d 100644
--- a/Annex/SpecialRemote/Config.hs
+++ b/Annex/SpecialRemote/Config.hs
@@ -116,6 +116,23 @@ annexObjectsField = Accepted "annexobjects"
 annexObjects :: ParsedRemoteConfig -> Bool
 annexObjects = fromMaybe False . getRemoteConfigValue annexObjectsField
 
+{- This is the order used in examples. Putting the typeField first
+ - in particular makes it easier to understand a RemoteConfig. -}
+conventionalConfigOrder :: RemoteConfigField -> RemoteConfigField -> Ordering
+conventionalConfigOrder a b = compare (n a) (n b)
+  where
+	n :: RemoteConfigField -> (Int, RemoteConfigField)
+	n f
+		| f == typeField       = (1, f)
+		| f == exportTreeField = (3, f)
+		| f == importTreeField = (4, f)
+		--                        5
+		| f == autoEnableField = (6, f)
+		| f == chunkField      = (7, f)
+		| f == chunksizeField  = (8, f)
+		| f == encryptionField = (9, f)
+		| otherwise            = (5, f)
+
 {- Parsers for fields that are common to all special remotes. -}
 commonFieldParsers :: [RemoteConfigFieldParser]
 commonFieldParsers =
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 3901544704..34b404b825 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -240,6 +240,10 @@ genRemoteConfigId = RemoteConfigId
 	go (f, v) = [fromProposedAccepted f, fromProposedAccepted v]
 
 displayRemoteConfig :: RemoteConfig -> String
-displayRemoteConfig = intercalate " " 
-	. map (\(f, v) -> fromProposedAccepted f ++ "=" ++ fromProposedAccepted v)
-	. M.toList
+displayRemoteConfig = intercalate " " . map display . sortBy order . M.toList
+  where
+	display (f, v) =
+		fromProposedAccepted f ++ "=" ++ fromProposedAccepted v
+	order (f1, v1) (f2, v2) = 
+		conventionalConfigOrder f1 f2 <> compare v1 v2
+
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index a391e10987..0b70077c9a 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -83,3 +83,10 @@ that any fix to the main attack will also close off this one.)
   The hash guarantees that what gets verified is what the user saw.
   What should be possible is to have a simple counter, rather than a hash,
   with a log of verification prompts that the counter indexes into.
+* Think about how to handle an update to a remote's encryption.
+  It might make sense to copy the hiddenEncryptionFields from the
+  git-annex branch remote.log even then otherwise using a verified
+  RemoteConfig. These fields can't be used to exploit this security hole,
+  and needing to run git-annex verifyremote to update them is weird,
+  and seems to have no security benefit since the user doesn't get a chance
+  to review them anyway.

fix handling of --sameas remotes in verified remote log
Store the config under the config-uuid, otherwise there was no way to
distinguish which of the pair of sameas remotes has been verified.
This makes the verified remote log includes the sameas inherited fields.
That's necessary because it does not include remotes that have not been
verified, so won't always include the remote that they were inherited from.
So, don't call removeSameasInherited in Logs.VerifiedRemote, and
also avoid addSameasInherited when loading it.
diff --git a/Annex/Branch.hs b/Annex/Branch.hs
index fba3356038..f4a95b0da4 100644
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -301,7 +301,7 @@ updateTo' pairs = do
 					else do
 						config <- Annex.getGitConfig
 						trustmap <- calcTrustMap <$> getStaged trustLog
-						remoteconfigmap <- calcRemoteConfigMap <$> getStaged remoteLog
+						remoteconfigmap <- calcRemoteConfigMapInherited <$> getStaged remoteLog
 						return $ map (\c -> c trustmap remoteconfigmap config) tcs
 		return $ UpdateFailedPermissions 
 			{ refsUnmerged = refs
@@ -875,7 +875,7 @@ performTransitionsLocked jl ts neednewlocalbranch transitionedrefs = do
 	run changers = do
 		config <- Annex.getGitConfig
 		trustmap <- calcTrustMap <$> getStaged trustLog
-		remoteconfigmap <- calcRemoteConfigMap <$> getStaged remoteLog
+		remoteconfigmap <- calcRemoteConfigMapInherited <$> getStaged remoteLog
 		-- partially apply, improves performance
 		let changers' = map (\c -> c trustmap remoteconfigmap config) changers
 		(fs, cleanup) <- branchFiles
diff --git a/CmdLine/GitRemoteAnnex.hs b/CmdLine/GitRemoteAnnex.hs
index d32fbae12c..049ca7f489 100644
--- a/CmdLine/GitRemoteAnnex.hs
+++ b/CmdLine/GitRemoteAnnex.hs
@@ -686,7 +686,8 @@ importTreeWorkAround rmt
 		m <- Logs.Remote.remoteConfigMap
 		r <- Remote.getRepo rmt
 		u <- getRepoUUID r
-		remoteGen' adjustconfig adjustReadOnly m (Remote.remotetype rmt) r u >>= \case
+		gc <- Annex.getRemoteGitConfig r
+		remoteGen' adjustconfig adjustReadOnly m (Remote.remotetype rmt) r gc u >>= \case
 			Just rmt' -> return rmt'
 			Nothing -> giveup "Failed to use importtree=yes remote."
   where
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index c5da44300a..3901544704 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -13,6 +13,7 @@ import Command
 import qualified Logs.Remote
 import qualified Logs.VerifiedRemote
 import qualified Remote
+import qualified Types.Remote as Remote
 import Types.RemoteConfig
 import Types.ProposedAccepted
 import Annex.SpecialRemote.Config
@@ -80,8 +81,8 @@ start o = do
 startVerify :: Remote -> SeekInput -> CommandStart
 startVerify r si = do
 	showCustom "verifyremote" si $ do
-		remoteconfig <- getUnverifiedRemoteConfig (Remote.uuid r)
-		verifiedremoteconfig <- getVerifiedRemoteConfig (Remote.uuid r)
+		remoteconfig <- getUnverifiedRemoteConfig (logUUID r)
+		verifiedremoteconfig <- getVerifiedRemoteConfig (logUUID r)
 		msg <- case remoteVerificationMessage' (Remote.name r) remoteconfig verifiedremoteconfig of
 			Just msg -> return msg
 			Nothing -> if verifiedremoteconfig == remoteconfig
@@ -89,7 +90,7 @@ startVerify r si = do
 				else do
 					-- Store new config as verified so that any
 					-- changes to the omitfields take effect.
-					Logs.VerifiedRemote.configSet (Remote.uuid r) remoteconfig
+					Logs.VerifiedRemote.configSet (logUUID r) remoteconfig
 					return "No verification is necessary; updated the remote's configuration."			
 		showRaw (encodeBS msg)
 		return True
@@ -151,10 +152,10 @@ remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
 
 performVerifySafe :: RemoteConfigId -> Remote -> CommandPerform
 performVerifySafe cid r = do
-	remoteconfig <- getUnverifiedRemoteConfig (Remote.uuid r)
+	remoteconfig <- getUnverifiedRemoteConfig (logUUID r)
 	if genRemoteConfigId remoteconfig == cid
 		then do
-			Logs.VerifiedRemote.configSet (Remote.uuid r) remoteconfig
+			Logs.VerifiedRemote.configSet (logUUID r) remoteconfig
 			next $ return True
 		else do
 			warning "invalid ID"
@@ -163,7 +164,7 @@ performVerifySafe cid r = do
 performVerifyUnsafe :: Remote -> CommandPerform
 performVerifyUnsafe r = do
 	m <- fromMaybe mempty <$> Logs.VerifiedRemote.readRemoteLog
-	let hasverifiedconfig = M.member (Remote.uuid r) m
+	let hasverifiedconfig = M.member (logUUID r) m
 	warning $ UnquotedString $ unlines $ 
 		warningVerifyUnsafe (Remote.name r) hasverifiedconfig
 	next $ return False
@@ -216,6 +217,10 @@ getVerifiedRemoteConfig u = do
 	m <- fromMaybe mempty <$> Logs.VerifiedRemote.readRemoteLog
 	return $ fromMaybe mempty (M.lookup u m)
 
+logUUID :: Remote -> UUID
+logUUID r = fromMaybe (Remote.uuid r) $
+	remoteAnnexConfigUUID (Remote.gitconfig r)
+
 {- The RemoteConfig is hashed to generate the RemoteConfigId. This
  - ensures that the config does not change between the verification
  - display and marking the config as safe.
diff --git a/Logs/Remote.hs b/Logs/Remote.hs
index c70d3389c6..fa967562dd 100644
--- a/Logs/Remote.hs
+++ b/Logs/Remote.hs
@@ -59,5 +59,5 @@ remoteConfigMap = maybe load return =<< Annex.getState Annex.remoteconfigmap
 		return m
 
 readRemoteLog :: Annex (M.Map UUID RemoteConfig)
-readRemoteLog = calcRemoteConfigMap 
+readRemoteLog = calcRemoteConfigMapInherited
 	<$> Annex.Branch.get remoteLog
diff --git a/Logs/Remote/Pure.hs b/Logs/Remote/Pure.hs
index e60c18b1f3..d48c6529d8 100644
--- a/Logs/Remote/Pure.hs
+++ b/Logs/Remote/Pure.hs
@@ -7,6 +7,7 @@
 
 module Logs.Remote.Pure (
 	calcRemoteConfigMap,
+	calcRemoteConfigMapInherited,
 	parseRemoteConfigLog,
 	buildRemoteConfigLog,
         keyValToConfig,
@@ -31,9 +32,11 @@ import qualified Data.Attoparsec.ByteString as A
 import Data.ByteString.Builder
 
 calcRemoteConfigMap :: L.ByteString -> M.Map UUID RemoteConfig
-calcRemoteConfigMap = (\m -> M.map (addSameasInherited m) m)
-	. simpleMap
-	. parseRemoteConfigLog
+calcRemoteConfigMap = simpleMap . parseRemoteConfigLog
+
+calcRemoteConfigMapInherited :: L.ByteString -> M.Map UUID RemoteConfig
+calcRemoteConfigMapInherited = (\m -> M.map (addSameasInherited m) m)
+	. calcRemoteConfigMap
 
 parseRemoteConfigLog :: L.ByteString -> Log RemoteConfig
 parseRemoteConfigLog = parseLogOld remoteConfigParser
diff --git a/Logs/VerifiedRemote.hs b/Logs/VerifiedRemote.hs
index 4506d1bf23..7c23d93d1f 100644
--- a/Logs/VerifiedRemote.hs
+++ b/Logs/VerifiedRemote.hs
@@ -42,7 +42,7 @@ configSet' ucfgs = do
 		<$> readLogFile logf lck
 	writeLogFile' logf $ \tmp -> F.writeFile tmp $
 		toLazyByteString $ buildRemoteConfigLog $
-			foldl' (\v (u, cfg) -> changeLog c u (removeSameasInherited cfg) v) old ucfgs
+			foldl' (\v (u, cfg) -> changeLog c u cfg v) old ucfgs
 	Annex.changeState $ \s -> s { Annex.verifiedremoteconfigmap = Nothing }
 
 {- Map of remotes by uuid with their RemoteConfigs. Cached for speed.
@@ -77,7 +77,7 @@ readRemoteLog = do
  - explains this to the user.
  -}
 checkNeedReadOnly
-	:: [(UUID, Maybe RemoteName)]
+	:: [(UUID, RemoteGitConfig, Maybe RemoteName)]
 	-> (M.Map UUID RemoteConfig)
 	-- ^ Logs.Remote.remoteConfigMap
 	-> Annex (Maybe String)
@@ -99,11 +99,17 @@ checkNeedReadOnly rs configmap = remoteConfigMap >>= \case
 	-- In a newly initialized repository, there are not yet
 	-- any enabled remotes, and this creates an empty log file.
 	Nothing -> do
-		configSet' $
-			mapMaybe (\(u, _) -> (u, ) <$> M.lookup u configmap) rs
+		configSet' $ mapMaybe (copyfromconfigmap configmap) rs
 		return Nothing
   where
-	notinmap m (u, remotename)
-		| M.notMember u configmap = Nothing
-		| M.member u m = Nothing
+	notinmap m (u, gc, remotename)
+		| M.notMember u' configmap = Nothing
+		| M.member u' m = Nothing
 		| otherwise = remotename
+	  where
+		u' = getcu u gc
+
+	copyfromconfigmap m (u, gc, _) =
+		(getcu u gc, ) <$> M.lookup u m
+	
+	getcu u gc = fromMaybe u (remoteAnnexConfigUUID gc)
diff --git a/Remote/List.hs b/Remote/List.hs
index 08585c6059..9677da87d4 100644
--- a/Remote/List.hs
+++ b/Remote/List.hs
@@ -84,7 +84,7 @@ remoteList' autoinit = do
 	m <- Logs.Remote.remoteConfigMap
 	l <- concat <$> mapM enumerate' remoteTypes
 
-	let usnames = map (\(u, g, _) -> (u, Git.remoteName g)) l
+	let usnames = map (\(u, g, gc, _) -> (u, gc, Git.remoteName g)) l
 	adjustremote <- maybe adjustReadOnly forceReadOnly
 		<$> Logs.VerifiedRemote.checkNeedReadOnly usnames m
 	
@@ -99,10 +99,11 @@ remoteList' autoinit = do
 		gs <- enumerate t autoinit
 		forM gs $ \g -> do

(Diff truncated)
bug
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index a391e10987..012b513e67 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -83,3 +83,6 @@ that any fix to the main attack will also close off this one.)
   The hash guarantees that what gets verified is what the user saw.
   What should be possible is to have a simple counter, rather than a hash,
   with a log of verification prompts that the counter indexes into.
+* Fix handling of sameas remotes in the verified remote log. Currently
+  the sameas remote's config is stored under theuuid,
+  rather than under the config-uuid.

fix verifyremote --unsafe message
It was incorrect if it is run *after* a remote was already verified
with --safe.
And it was incorrect if it was run after upgrading from an old version
of git-annex which automatically makes the remote be verified.
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index c70fa04355..c5da44300a 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -169,9 +169,9 @@ performVerifyUnsafe r = do
 	next $ return False
 
 warningVerifyUnsafe :: RemoteName -> Bool -> [String]
-warningVerifyUnsafe name True =
+warningVerifyUnsafe name False =
 	[ "So the remote " ++ name ++ " is in a location you did"
-	, "not expect to be using. You appear to be under attack."
+	, "not expect to be using. You may be under attack."
 	, ""
 	, "No annexed files have been sent to that remote or any other"
 	, "remote since it was enabled, and git pushes have also been"
@@ -196,12 +196,14 @@ warningVerifyUnsafe name True =
 	, "with this attack, you can unblock by running:"
 	, "\tgit remote remove " ++ name
 	]
-warningVerifyUnsafe name False =
+warningVerifyUnsafe name True =
 	[ "Unfortunately, information may have leaked about the content"
-	, "of " ++ name ++ " since it was enabled by a previous version"
-	, "of git-annex that did not verify the location of special remotes"
-	, "before using them. You should assume that other remotes have"
-	, "been sent the content of annexed files in " ++ name ++ "."
+	, "of " ++ name ++ "."
+	, ""
+	, "Either it has already been verified as safe, or it was was"
+	, "enabled by a previous version of git-annex that did not verify"
+	, "the location of special remotes. So nothing prevented content"
+	, "from it being sent to other remotes."
 	]
 
 getUnverifiedRemoteConfig :: UUID -> Annex RemoteConfig
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index 16049ecd8c..a391e10987 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -66,8 +66,10 @@ that any fix to the main attack will also close off this one.)
   annex objects leaking out of it to other remotes. (done)
 * What to do about already running processes that could send annex objects
   that get received from the special remote on to other remotes?
+  Perhaps this would best be avoided by not allowing enabling special
+  remotes when there are other git-annex processes running.
 * Disable all git pushes (using git pre-push hook), to avoid
-  presence information leaking out of it.
+  presence information leaking out.
 * when using a remote and remote.log has a changed RemoteConfig,
   warn user that the configuration has changed and suggest running
   git-annex verifyremote. How to avoid warning too often or not often
@@ -81,7 +83,3 @@ that any fix to the main attack will also close off this one.)
   The hash guarantees that what gets verified is what the user saw.
   What should be possible is to have a simple counter, rather than a hash,
   with a log of verification prompts that the counter indexes into.
-* verifyremote --unsafe message is incorrect if it is run *after* a remote
-  was already verified with --safe. It is also incorrect if it is run
-  after upgrading from an old version of git-annex which automatically
-  makes the config verified.

force remotes readonly when there is an unverified special remote
Also this handles upgrades from past versions of git-annex, populating
the verified remote log when it does not exist.
Added a test case to make sure that when git-annex init
autoenables a remote, the upgrade logic does not copy it into the
verified remote log.
Pushes to remotes are not yet prevented. It would be possible to set
make forceReadOnly set remoteAnnexReadOnly, which would make git-annex
sync, the assistant, etc not push. But they would silently skip pushing,
and there needs to be a message informing the user why. And since any
git push needs to be prevented, that needs to be done with a pre-push
hook instead.
diff --git a/Annex.hs b/Annex.hs
index e10e306040..a707cae00d 100644
--- a/Annex.hs
+++ b/Annex.hs
@@ -208,6 +208,7 @@ data AnnexState = AnnexState
 	, preferredcontentmap :: Maybe (FileMatcherMap Annex)
 	, requiredcontentmap :: Maybe (FileMatcherMap Annex)
 	, remoteconfigmap :: Maybe (M.Map UUID RemoteConfig)
+	, verifiedremoteconfigmap :: Maybe (Maybe (M.Map UUID RemoteConfig))
 	, clusters :: Maybe (Annex Clusters)
 	, maxsizes :: Maybe (M.Map UUID MaxSize)
 	, forcetrust :: TrustMap
@@ -268,6 +269,7 @@ newAnnexState c r = do
 		, preferredcontentmap = Nothing
 		, requiredcontentmap = Nothing
 		, remoteconfigmap = Nothing
+		, verifiedremoteconfigmap = Nothing
 		, clusters = Nothing
 		, maxsizes = Nothing
 		, forcetrust = M.empty
diff --git a/CmdLine/GitRemoteAnnex.hs b/CmdLine/GitRemoteAnnex.hs
index 31c4213506..d32fbae12c 100644
--- a/CmdLine/GitRemoteAnnex.hs
+++ b/CmdLine/GitRemoteAnnex.hs
@@ -52,6 +52,7 @@ import Annex.Tmp
 import Annex.SpecialRemote.Config
 import Remote.List
 import Remote.List.Util
+import Remote.Helper.ReadOnly
 import Utility.Tmp
 import Utility.Tmp.Dir
 import Utility.Env
@@ -684,7 +685,8 @@ importTreeWorkAround rmt
 	| otherwise = do
 		m <- Logs.Remote.remoteConfigMap
 		r <- Remote.getRepo rmt
-		remoteGen' adjustconfig m (Remote.remotetype rmt) r >>= \case
+		u <- getRepoUUID r
+		remoteGen' adjustconfig adjustReadOnly m (Remote.remotetype rmt) r u >>= \case
 			Just rmt' -> return rmt'
 			Nothing -> giveup "Failed to use importtree=yes remote."
   where
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index e73efbff9a..c70fa04355 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -162,7 +162,7 @@ performVerifySafe cid r = do
 
 performVerifyUnsafe :: Remote -> CommandPerform
 performVerifyUnsafe r = do
-	m <- Logs.VerifiedRemote.readRemoteLog
+	m <- fromMaybe mempty <$> Logs.VerifiedRemote.readRemoteLog
 	let hasverifiedconfig = M.member (Remote.uuid r) m
 	warning $ UnquotedString $ unlines $ 
 		warningVerifyUnsafe (Remote.name r) hasverifiedconfig
@@ -211,7 +211,7 @@ getUnverifiedRemoteConfig u = do
 
 getVerifiedRemoteConfig :: UUID -> Annex RemoteConfig
 getVerifiedRemoteConfig u = do
-	m <- Logs.VerifiedRemote.readRemoteLog
+	m <- fromMaybe mempty <$> Logs.VerifiedRemote.readRemoteLog
 	return $ fromMaybe mempty (M.lookup u m)
 
 {- The RemoteConfig is hashed to generate the RemoteConfigId. This
diff --git a/Logs/Remote.hs b/Logs/Remote.hs
index 3fb90092c8..c70d3389c6 100644
--- a/Logs/Remote.hs
+++ b/Logs/Remote.hs
@@ -40,21 +40,22 @@ configSet u cfg = do
 			. parseRemoteConfigLog
 	Annex.changeState $ \s -> s { Annex.remoteconfigmap = Nothing }
 
-{- Map of remotes by uuid with their RemoteConfigs.
+{- Map of remotes by uuid with their RemoteConfigs. Cached for speed.
  -
  - When the verified remote log has a RemoteConfig, it is used, otherwise
  - the value from the git-annex branch is used. This ensures that, once a
  - user has verified a RemoteConfig, it does not change unexpectedly.
- -
- - Cached for speed. -}
+ -}
 remoteConfigMap :: Annex (M.Map UUID RemoteConfig)
 remoteConfigMap = maybe load return =<< Annex.getState Annex.remoteconfigmap
   where
 	load = do
 		logged <- readRemoteLog
-		verified <- Logs.VerifiedRemote.readRemoteLog
+		verified <- fromMaybe mempty
+			<$> Logs.VerifiedRemote.remoteConfigMap
 		let m = M.union verified logged
-		Annex.changeState $ \s -> s { Annex.remoteconfigmap = Just m }
+		Annex.changeState $ \s ->
+			s { Annex.remoteconfigmap = Just m }
 		return m
 
 readRemoteLog :: Annex (M.Map UUID RemoteConfig)
diff --git a/Logs/VerifiedRemote.hs b/Logs/VerifiedRemote.hs
index 0b7318707c..4506d1bf23 100644
--- a/Logs/VerifiedRemote.hs
+++ b/Logs/VerifiedRemote.hs
@@ -9,9 +9,13 @@
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE TupleSections #-}
+
 module Logs.VerifiedRemote (
+	remoteConfigMap,
 	readRemoteLog,
 	configSet,
+	checkNeedReadOnly,
 ) where
 
 import Annex.Common
@@ -27,7 +31,10 @@ import qualified Data.Map as M
 import Data.ByteString.Builder
 
 configSet :: UUID -> RemoteConfig -> Annex ()
-configSet u cfg = do
+configSet u c = configSet' [(u, c)]
+
+configSet' :: [(UUID, RemoteConfig)] -> Annex ()
+configSet' ucfgs = do
 	c <- currentVectorClock
 	logf <- fromRepo gitAnnexVerifiedRemoteLog
 	lck <- fromRepo gitAnnexVerifiedRemoteLock
@@ -35,10 +42,68 @@ configSet u cfg = do
 		<$> readLogFile logf lck
 	writeLogFile' logf $ \tmp -> F.writeFile tmp $
 		toLazyByteString $ buildRemoteConfigLog $
-			changeLog c u (removeSameasInherited cfg) old
+			foldl' (\v (u, cfg) -> changeLog c u (removeSameasInherited cfg) v) old ucfgs
+	Annex.changeState $ \s -> s { Annex.verifiedremoteconfigmap = Nothing }
+
+{- Map of remotes by uuid with their RemoteConfigs. Cached for speed.
+ -
+ - Nothing when the verified remote config log file does not exist yet.
+ -}
+remoteConfigMap :: Annex (Maybe (M.Map UUID RemoteConfig))
+remoteConfigMap =
+	maybe load return =<< Annex.getState Annex.verifiedremoteconfigmap
+  where
+	load = do
+		m <- readRemoteLog
+		Annex.changeState $ \s ->
+			s { Annex.verifiedremoteconfigmap = Just m }
+		return m
 
-readRemoteLog :: Annex (M.Map UUID RemoteConfig)
+readRemoteLog :: Annex (Maybe (M.Map UUID RemoteConfig))
 readRemoteLog = do
 	logf <- fromRepo gitAnnexVerifiedRemoteLog
 	lck <- fromRepo gitAnnexVerifiedRemoteLock
-	maybe mempty calcRemoteConfigMap <$> readLogFile logf lck
+	fmap calcRemoteConfigMap <$> readLogFile logf lck
+
+{- Given a list of UUIDs of all currently enabled remotes, check if any of
+ - them are not in the verified remote log, in which case all remotes need
+ - to be made read-only.
+ -
+ - This is to prevent exfiltration of data from an unverified remote
+ - that accesses a resource that the user did not expect, to an
+ - attacker controlled remote.
+ - 
+ - When remotes need to be made read-only, a message is returned which
+ - explains this to the user.
+ -}
+checkNeedReadOnly
+	:: [(UUID, Maybe RemoteName)]
+	-> (M.Map UUID RemoteConfig)
+	-- ^ Logs.Remote.remoteConfigMap
+	-> Annex (Maybe String)
+checkNeedReadOnly rs configmap = remoteConfigMap >>= \case
+	Just m -> return $ case mapMaybe (notinmap m) rs of
+		[] -> Nothing
+		(remotename:_) -> Just $ unwords
+			[ "Write prevented due to an unverified special"
+			, "remote. Run this command to verify it:"
+			, "git-annex verifyremote " ++ remotename
+			]
+	-- The log file does not exist yet. This is either a newly
+	-- initialized repository, or git-annex has been upgraded from a
+	-- version that did not use this log.
+	--
+	-- Handle upgrade by populating the log with the
+	-- current configs of all the currently enabled remotes.
+	--
+	-- In a newly initialized repository, there are not yet
+	-- any enabled remotes, and this creates an empty log file.
+	Nothing -> do
+		configSet' $
+			mapMaybe (\(u, _) -> (u, ) <$> M.lookup u configmap) rs
+		return Nothing
+  where
+	notinmap m (u, remotename)
+		| M.notMember u configmap = Nothing
+		| M.member u m = Nothing
+		| otherwise = remotename
diff --git a/Remote/External.hs b/Remote/External.hs
index 2197338591..4ffbbf3221 100644

(Diff truncated)
fix wording
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
index 5eee73d7d4..fe76d708d3 100644
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -1619,7 +1619,7 @@ Remotes are configured using these settings in `.git/config`.
   If set to `true`, prevents git-annex from making changes to a remote.
   This prevents `git-annex sync` and `git-annex assist` from pushing
   changes to a git repository. And it prevents storing or removing
-  files from read-only remote.
+  files.
 
 * `remote.<name>.annex-verify`, `annex.verify`
 

upgrades
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index b6825c5cd3..8590c7b402 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -60,6 +60,7 @@ that any fix to the main attack will also close off this one.)
   remote configuration as specified by the user, and save it as
   the new verified configuration, and to the git-annex branch
   (done)
+* handle upgrades
 * When any enabled special remote does not have an entry in the verified
   remote log, disable annex objects transfers to any remotes, to avoid
   annex objects leaking out of it to other remotes.  
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
index 336a031c7c..e53ed28874 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
@@ -31,12 +31,4 @@ after the git-annex branch was updated with a newer config.
 
 To handle legitimate config changes, git-annex could notify the user about
 a config change and have them verify it before using it.
-
-How to handle upgrades, where it's not yet recorded which config a special
-remote was enabled with? It could default to the first or the last
-remote.log config of the special remote. First would be undesirable when
-git-annex enableremote has been used in the past to change the config
-in some signficant way. Last risks exposing the user of an upgraded
-git-annex to this security hole, but only if an attacker has already
-updated the git-annex branch.
 """]]
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_7_b4dbf3e93b623b0cc960d66d00098fe2._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_7_b4dbf3e93b623b0cc960d66d00098fe2._comment
new file mode 100644
index 0000000000..db6bb949f0
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_7_b4dbf3e93b623b0cc960d66d00098fe2._comment
@@ -0,0 +1,34 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 7"""
+ date="2026-10-14T14:24:27Z"
+ content="""
+How to handle upgrades?
+
+The choices seem to be:
+
+1. Do nothing. All special remotes will be unverified, and so all pushes
+   will be prevented until the user verifies them.
+2. Register existing special remotes as verified automatically on upgrade. 
+   Recommend that the user check the special remotes that they have already
+   enabled, which they can do with `git-annex verifyremote`.
+
+The big problem with #1 is that people have automated systems that are
+working. If upgrading git-annex requires a manual step to keep them
+working, and missing that causes existing data flows stop happening,
+it's entirely possible that it will lead to data loss. This makes me
+deeply uncomfortable with #1. A fairly low impact security hole should
+not lead to a high risk of data loss.
+
+And if someone discovered this security hole already and has been
+exploiting it, then they will have probably already obtained all the
+annexed objects in the special remote. So #1 feels like locking the door
+after the cows are out.
+
+If someone learns about this security hole from the advisory and starts trying
+to exploit it, it will be down to a race between them and when git-annex gets
+upgraded. But, if a user has just enabled a special remote and then
+learns about this security hole, they have an obvious thing to check.
+
+So, I think #2 is the best choice.
+"""]]
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
index e68c8120c6..8f1f4130aa 100644
--- a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -1,7 +1,8 @@
+(FIXME: CVE number assignment)
 git-annex was vulnerable to a private data exfiltration attack. Joey Hess
 discovered this attack.
 
-These security holes were fixed in git-annex (FIXME: version)
+This security hole was fixed in git-annex (FIXME: version)
 
 ## attack descriptions
 
@@ -32,11 +33,16 @@ from their special remote and sent to the attacker's git-annex repository.
 
 ## git-annex security fixes
 
-To avoid these attacks, after a special remote is enabled, git-annex
-will refuse to send annexed files to remotes, and will also block git
-pushes, until the user verifies that the configuration of the 
-special remote is safe.
+To avoid these attacks, after a new special remote is enabled, git-annex
+will refuse to send annexed files to all remotes, and will also block git
+pushes, until the user verifies that the configuration of the special
+remote is safe.
 
 Additionally, changes to the configuration of already enabled 
 special remotes, that are received via the git-annex branch,
 will not take effect until the user has verified they are safe.
+
+After upgrading git-annex, existing special remotes will continue to work
+as before. If a special remote could have been configured by a third party,
+you should verify its configuration using the `git-annex verifyremote`
+command.

verifyremote upgrade handling
When a special remote was enabled by a previous version of git-annex,
there will be no config for it in the verified remote log. Display a
different message to the user then on --unsafe, to let them know the bad
news.
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 67585c4975..1a435b4e7b 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -158,11 +158,14 @@ performVerifySafe cid r = do
 
 performVerifyUnsafe :: Remote -> CommandPerform
 performVerifyUnsafe r = do
-	warnVerifyUnsafe r
+	m <- Logs.VerifiedRemote.readRemoteLog
+	let hasverifiedconfig = M.member (Remote.uuid r) m
+	warning $ UnquotedString $ unlines $ 
+		warningVerifyUnsafe (Remote.name r) hasverifiedconfig
 	next $ return False
 
-warnVerifyUnsafe :: Remote -> Annex ()
-warnVerifyUnsafe r = warning $ UnquotedString $ unlines
+warningVerifyUnsafe :: RemoteName -> Bool -> [String]
+warningVerifyUnsafe name True =
 	[ "So the remote " ++ name ++ " is in a location you did"
 	, "not expect to be using. You appear to be under attack."
 	, ""
@@ -189,8 +192,13 @@ warnVerifyUnsafe r = warning $ UnquotedString $ unlines
 	, "with this attack, you can unblock by running:"
 	, "\tgit remote remove " ++ name
 	]
-  where
-	name = Remote.name r
+warningVerifyUnsafe name False =
+	[ "Unfortunately, information may have leaked about the content"
+	, "of " ++ name ++ " since it was enabled by a previous version"
+	, "of git-annex that did not verify the location of special remotes"
+	, "before using them. You should assume that other remotes have"
+	, "been sent the content of annexed files in " ++ name ++ "."
+	]
 
 getUnverifiedRemoteConfig :: UUID -> Annex RemoteConfig
 getUnverifiedRemoteConfig u = do
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index 45a32136ad..b6825c5cd3 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -66,13 +66,6 @@ that any fix to the main attack will also close off this one.)
   What to do about already running processes that could send annex objects?
 * And disable all git pushes (using git pre-push hook), to avoid
   presence information leaking out of it.
-* handle upgrades from past git-annex versions, by recording something
-  in the verified remote log for enabled special remotes.  
-  Is the current RemoteConfig safe enough to record?
-* `git-annex verifyremote --unsafe` will claim that no annexed
-  files or git pushes have been sent since the remote was enabled,
-  which is not true if it was initialized by an older version of
-  git-annex
 * when using a remote and remote.log has a changed RemoteConfig,
   warn user that the configuration has changed and suggest running
   git-annex verifyremote. How to avoid warning too often or not often
@@ -80,3 +73,9 @@ that any fix to the main attack will also close off this one.)
   on their own when they want to verify the new configuration?
 * Maybe make verifyremote store the config that was shown to the user,
   avoiding the need to copy and paste the config hash.
+  But, bear in mind that the current design is safe even if git-annex
+  branch changes to the remote configuration arrive, and another process
+  runs enableremote, before the user runs `verifyremote --safe`.
+  The hash guarantees that what gets verified is what the user saw.
+  What should be possible is to have a simple counter, rather than a hash,
+  with a log of verification prompts that the counter indexes into.
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
index 3a668185a6..e68c8120c6 100644
--- a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -32,14 +32,11 @@ from their special remote and sent to the attacker's git-annex repository.
 
 ## git-annex security fixes
 
-To avoid these attacks, after autoenabling a special remote, git-annex
+To avoid these attacks, after a special remote is enabled, git-annex
 will refuse to send annexed files to remotes, and will also block git
-pushes, until the user verifies that the configuration of the autoenabled
+pushes, until the user verifies that the configuration of the 
 special remote is safe.
 
-Also, `git-annex enableremote` will display the configuration of the
-special remote so that the user can verify if it is safe.
-
-Additionally, changes to the configuration of special remotes that
-are received via the git-annex branch will not take effect until the user
-has verified they are safe.
+Additionally, changes to the configuration of already enabled 
+special remotes, that are received via the git-annex branch,
+will not take effect until the user has verified they are safe.

enableremote verifyremote integration
When enabling a new remote, display its configuration for verification.
When enabling an existing remote, store any changes to its configuration
to the verified remote log.
Note that, if the git-annex branch has a configuration change for a
remote that has not yet been verified, enableremote will take the
previously verified configuration, add the user's config changes to
it, and store that back to the git-annex branch. So overwriting the
unverified configuration change. It is desirable to have a way to
overwrite an unverified configuration change, eg to revert a bad change.
If the user is not aware of the configuration change, they will
silently overwrite it. But, it was always possible for enableremote to
overwrite a configuration change made elsewhere. In the past, the
git-annex branch with that change had to not yet be available, then it
would be silently overwritten. So the only real difference is that this
can overwrite a change that has already been received via the git-annex
branch.
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
index ae122ee568..2466517396 100644
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -12,6 +12,7 @@ module Command.EnableRemote where
 import Command
 import qualified Annex
 import qualified Logs.Remote
+import qualified Logs.VerifiedRemote
 import qualified Types.Remote as R
 import qualified Git
 import qualified Annex.SpecialRemote as SpecialRemote
@@ -24,6 +25,7 @@ import Config
 import Config.DynamicConfig
 import Types.GitConfig
 import Types.ProposedAccepted
+import Command.VerifyRemote (remoteVerificationMessage)
 
 import qualified Data.Map as M
 
@@ -116,14 +118,18 @@ startSpecialRemote' _ _ _ _ _ =
 
 performSpecialRemote :: EnableRemoteOptions -> PerformSpecialRemote
 performSpecialRemote o t u name oldc c gc mcu = do
-	-- Avoid enabling a special remote if there is another remote
-	-- with the same name.
 	rs <- Remote.remoteList
-	case filter (\rmt -> Remote.name rmt == name) rs of
-		(rmt:_) | Remote.uuid rmt /= u ->
-			giveup $ "Not overwriting currently configured git remote named \"" ++ name ++ "\""
-		_ -> noop
+	prevrmt <- case filter (\rmt -> Remote.name rmt == name) rs of
+		(rmt:_) -> return (Just rmt)
+		_ -> return Nothing
+	unless (maybe u Remote.uuid prevrmt == u) $
+		giveup $ "Not overwriting currently configured git remote named \"" ++ name ++ "\""
 	(c', u') <- R.setup t (R.Enable oldc) (Just u) name Nothing c gc
+	if prevrmt == Nothing
+		then remoteVerificationMessage name u c' >>= \case
+			Just msg -> showLongNote (UnquotedString msg)
+			Nothing -> noop
+		else Logs.VerifiedRemote.configSet u' c 
 	next $ cleanupSpecialRemote o t u' c' mcu
 
 cleanupSpecialRemote :: EnableRemoteOptions -> RemoteType -> UUID -> R.RemoteConfig -> Maybe (SpecialRemote.ConfigFrom UUID) -> CommandCleanup
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
index 30e6a3ac67..67585c4975 100644
--- a/Command/VerifyRemote.hs
+++ b/Command/VerifyRemote.hs
@@ -95,11 +95,10 @@ startVerify r si = do
 		return True
 	stop
 
-remoteVerificationMessage :: RemoteName -> UUID -> Annex (Maybe String)
-remoteVerificationMessage remotename u = 
-	remoteVerificationMessage' remotename
-		<$> getUnverifiedRemoteConfig u
-		<*> getVerifiedRemoteConfig u
+remoteVerificationMessage :: RemoteName -> UUID -> RemoteConfig -> Annex (Maybe String)
+remoteVerificationMessage remotename u remoteconfig = 
+	remoteVerificationMessage' remotename remoteconfig
+		<$> getVerifiedRemoteConfig u
 
 remoteVerificationMessage' :: RemoteName -> RemoteConfig -> RemoteConfig -> Maybe String
 remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index d31fd1b5e5..45a32136ad 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -54,14 +54,12 @@ that any fix to the main attack will also close off this one.)
   preferred over git-annex branch remote.log (done)
 * initremote populate verified remote log (done)
 * implement git-annex verifyremote (done)
-* when using a remote and remote.log has a changed RemoteConfig,
-  warn user that the configuration has changed and suggest running
-  git-annex verifyremote. How to avoid warning too often or not often
-  enough?
-* when autoenabling a special remote, warn user that they should
-  run git-annex verifyremote.
-* enableremote should display remote configuration and tell user to
-  check it and run git-annex verifyremote.
+* enableremote of a new remote should display remote configuration and
+  tell user to check it and run git-annex verifyremote (done)
+* enableremote of an existing remote should modify the (verified)
+  remote configuration as specified by the user, and save it as
+  the new verified configuration, and to the git-annex branch
+  (done)
 * When any enabled special remote does not have an entry in the verified
   remote log, disable annex objects transfers to any remotes, to avoid
   annex objects leaking out of it to other remotes.  
@@ -75,3 +73,10 @@ that any fix to the main attack will also close off this one.)
   files or git pushes have been sent since the remote was enabled,
   which is not true if it was initialized by an older version of
   git-annex
+* when using a remote and remote.log has a changed RemoteConfig,
+  warn user that the configuration has changed and suggest running
+  git-annex verifyremote. How to avoid warning too often or not often
+  enough? Or, maybe this is not needed, and the user can decide
+  on their own when they want to verify the new configuration?
+* Maybe make verifyremote store the config that was shown to the user,
+  avoiding the need to copy and paste the config hash.
diff --git a/doc/git-annex-verifyremote.mdwn b/doc/git-annex-verifyremote.mdwn
index a404803660..a70bf0c971 100644
--- a/doc/git-annex-verifyremote.mdwn
+++ b/doc/git-annex-verifyremote.mdwn
@@ -16,7 +16,7 @@ other remotes.
 To prevent such data leakage, after a special remote is enabled,
 either by `git-annex enableremote` or automatically,
 git-annex will prevent sending annexed files, or git pushes to any
-remote, until this command verifies that it's safe.
+remote, until this command is used to verify that it's safe.
 
 This command can also be used when the configuration of a special remote
 has changed. The old configuration will continue to be used until 

add git-annex verifyremote command
diff --git a/Annex/SpecialRemote/Config.hs b/Annex/SpecialRemote/Config.hs
index 9d1ab13c7e..f9a03bbdcd 100644
--- a/Annex/SpecialRemote/Config.hs
+++ b/Annex/SpecialRemote/Config.hs
@@ -155,10 +155,6 @@ sameasInherits = S.fromList
 	-- encryption configuration is necessarily the same for two
 	-- remotes that access the same data store
 	[ encryptionField
-	, macField
-	, cipherField
-	, cipherkeysField
-	, pubkeysField
 	-- legacy chunking was either enabled or not, so has to be the same
 	-- across configs for remotes that access the same data
 	, chunksizeField
@@ -167,6 +163,16 @@ sameasInherits = S.fromList
 	-- for the sameas remote, since it's reading whatever chunks were
 	-- stored
 	, chunkField
+	] <> hiddenEncryptionFields
+
+{- Fields that are used for encrypted special remotes, and are not user
+ - visible. -}
+hiddenEncryptionFields :: S.Set RemoteConfigField
+hiddenEncryptionFields = S.fromList
+	[ macField
+	, cipherField
+	, cipherkeysField
+	, pubkeysField
 	]
 
 {- Each RemoteConfig that has a sameas-uuid inherits some fields
diff --git a/CmdLine/GitAnnex.hs b/CmdLine/GitAnnex.hs
index b2adcfa6f1..9a0bf41a22 100644
--- a/CmdLine/GitAnnex.hs
+++ b/CmdLine/GitAnnex.hs
@@ -56,6 +56,7 @@ import qualified Command.Init
 import qualified Command.Describe
 import qualified Command.InitRemote
 import qualified Command.EnableRemote
+import qualified Command.VerifyRemote
 import qualified Command.ConfigRemote
 import qualified Command.RenameRemote
 import qualified Command.DisableRemote
@@ -174,6 +175,7 @@ cmds testoptparser testrunner mkbenchmarkgenerator = map addGitAnnexCommonOption
 	, Command.Describe.cmd
 	, Command.InitRemote.cmd
 	, Command.EnableRemote.cmd
+	, Command.VerifyRemote.cmd
 	, Command.ConfigRemote.cmd
 	, Command.RenameRemote.cmd
 	, Command.DisableRemote.cmd
diff --git a/Command/VerifyRemote.hs b/Command/VerifyRemote.hs
new file mode 100644
index 0000000000..30e6a3ac67
--- /dev/null
+++ b/Command/VerifyRemote.hs
@@ -0,0 +1,227 @@
+{- git-annex command
+ -
+ - Copyright 2026 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Command.VerifyRemote where
+
+import Command
+import qualified Logs.Remote
+import qualified Logs.VerifiedRemote
+import qualified Remote
+import Types.RemoteConfig
+import Types.ProposedAccepted
+import Annex.SpecialRemote.Config
+import Utility.Hash
+
+import qualified Data.Map as M
+import qualified Data.Set as S
+
+cmd :: Command
+cmd = command "verifyremote" SectionSetup
+	"verify if a special remote is safe to use"
+	paramName
+	(seek <$$> optParser)
+
+data VerifyRemoteOptions = VerifyRemoteOptions
+	{ remote :: DeferredParse Remote
+	, safeunsafe :: Maybe SafeUnsafe
+	}
+
+data SafeUnsafe = Safe RemoteConfigId | Unsafe
+
+data RemoteConfigId = RemoteConfigId { fromRemoteConfigId :: String }
+	deriving (Eq)
+
+optParser :: CmdParamsDesc -> Parser VerifyRemoteOptions
+optParser _ = VerifyRemoteOptions
+	<$> (mkParseRemoteOption <$> remotearg)
+	<*> optional safeUnsafeParser
+  where
+	remotearg = strArgument
+		( metavar paramRemote
+		<> help "remote to verify"
+		<> completeRemotes
+		)
+
+safeUnsafeParser :: Parser SafeUnsafe
+safeUnsafeParser = safe <|> unsafe
+  where
+	safe = option (Safe . RemoteConfigId <$> str)
+		( long "safe" <> metavar "ID"
+		<> help "special remote configuration is safe"
+		)
+	unsafe = flag' Unsafe
+		( long "unsafe"
+		<> help "special remote configuration is unsafe"
+		)
+
+seek :: VerifyRemoteOptions -> CommandSeek
+seek = commandAction . start
+
+start :: VerifyRemoteOptions -> CommandStart
+start o = do
+	r <- getParsed (remote o)
+	let si = SeekInput [Remote.name r]
+	let ai = ActionItemUUID
+		(Remote.uuid r)
+		(UnquotedString (Remote.name r))
+	case safeunsafe o of
+		Just (Safe cid) -> starting "verifyremote" ai si $
+			performVerifySafe cid r
+		Just Unsafe -> starting "verifyremote" ai si $
+			performVerifyUnsafe r
+		Nothing -> startVerify r si
+
+startVerify :: Remote -> SeekInput -> CommandStart
+startVerify r si = do
+	showCustom "verifyremote" si $ do
+		remoteconfig <- getUnverifiedRemoteConfig (Remote.uuid r)
+		verifiedremoteconfig <- getVerifiedRemoteConfig (Remote.uuid r)
+		msg <- case remoteVerificationMessage' (Remote.name r) remoteconfig verifiedremoteconfig of
+			Just msg -> return msg
+			Nothing -> if verifiedremoteconfig == remoteconfig
+				then return "There have been no changes to the remote's configuration since it was last verified."
+				else do
+					-- Store new config as verified so that any
+					-- changes to the omitfields take effect.
+					Logs.VerifiedRemote.configSet (Remote.uuid r) remoteconfig
+					return "No verification is necessary; updated the remote's configuration."			
+		showRaw (encodeBS msg)
+		return True
+	stop
+
+remoteVerificationMessage :: RemoteName -> UUID -> Annex (Maybe String)
+remoteVerificationMessage remotename u = 
+	remoteVerificationMessage' remotename
+		<$> getUnverifiedRemoteConfig u
+		<*> getVerifiedRemoteConfig u
+
+remoteVerificationMessage' :: RemoteName -> RemoteConfig -> RemoteConfig -> Maybe String
+remoteVerificationMessage' remotename remoteconfig verifiedremoteconfig
+	| visiblefields verifiedremoteconfig == visibleremoteconfig = Nothing
+	| otherwise = Just $
+		if verifiedremoteconfig /= mempty
+			then updatemessage
+			else newmessage
+  where
+	newmessage = unlines
+		[ "This is the configuration of the remote:"
+		, "\t" ++ displayRemoteConfig visibleremoteconfig
+		, ""
+		, "Is this a location that you expected to be using?"
+		, "If so, run:  git-annex verifyremote "
+			 ++ remotename ++ " --safe "
+			 ++ fromRemoteConfigId cid
+		, "If not, run: git-annex verifyremote "
+			 ++ remotename ++ " --unsafe"
+		]
+	
+	updatemessage = unlines
+		[ "There is an updated configuration of the remote:"
+		, "\t" ++ displayRemoteConfig visibleremoteconfig
+		, ""
+		, "Is this a location that you expected to be using?"
+		, "If so, run:  git-annex verifyremote "
+			 ++ remotename ++ " --safe "
+			 ++ fromRemoteConfigId cid
+		, "If not, you do not need to do anything; the previously"
+		, "verified configuration of the remote will continue to"
+		, "be used."
+		]
+	
+	cid = genRemoteConfigId remoteconfig
+	
+	visibleremoteconfig = visiblefields remoteconfig
+
+	visiblefields = flip M.withoutKeys omitfields
+

(Diff truncated)
add checklist
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index c399f0fb95..2b975d8961 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -47,3 +47,31 @@ Still, while this seems too hard to be a viable attack, it seems likely
 that any fix to the main attack will also close off this one.) 
 
 --[[Joey]]
+
+## fix checklist
+
+* implement verified remote log, and make RemoteConfig values from it be
+  preferred over git-annex branch remote.log (done)
+* initremote populate verified remote log (done)
+* implement git-annex verifyremote
+* when using a remote and remote.log has a changed RemoteConfig,
+  warn user that the configuration has changed and suggest running
+  git-annex verifyremote. How to avoid warning too often or not often
+  enough?
+* when autoenabling a special remote, warn user that they should
+  run git-annex verifyremote.
+* enableremote should display remote configuration and tell user to
+  check it and run git-annex verifyremote.
+* When any enabled special remote does not have an entry in the verified
+  remote log, disable annex objects transfers to any remotes, to avoid
+  annex objects leaking out of it to other remotes.  
+  What to do about already running processes that could send annex objects?
+* And disable all git pushes (using git pre-push hook), to avoid
+  presence information leaking out of it.
+* handle upgrades from past git-annex versions, by recording something
+  in the verified remote log for enabled special remotes.  
+  Is the current RemoteConfig safe enough to record?
+* `git-annex verifyremote --unsafe` will claim that no annexed
+  files or git pushes have been sent since the remote was enabled,
+  which is not true if it was initialized by an older version of
+  git-annex

encrypted special remotes also vulnerable, when git repo is published
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_6_ac3297615c7e3a761ffad7744f1a2aa1._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_6_ac3297615c7e3a761ffad7744f1a2aa1._comment
new file mode 100644
index 0000000000..5d0759fbec
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_6_ac3297615c7e3a761ffad7744f1a2aa1._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 6"""
+ date="2026-10-12T16:17:16Z"
+ content="""
+Exfiltration from an encrypted special remote needs the attacker to know
+the encryption cipher.
+
+If I have an encrypted special remote, and I opt to publish the git
+repository, the attacker *does* have enough information for this attack.
+
+They can reuse the cipher field of the config of my special remote in
+their special remote. With encryption=shared, that's not encrypted and it
+obviously works. With encryption=pubkey or encryption=hybrid, it is
+encrypted, but my gpg key will decrypt it and be able to access files on my
+encrypted special remote.
+
+If I don't publish my git repository, my encrypted special remotes are safe
+from this attack.
+"""]]
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
index 5395fe4573..3a668185a6 100644
--- a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -30,8 +30,6 @@ own special remote (eg the victim's private S3 bucket). When the victim runs a
 command such as `git-annex sync --content`, their private files are retrieved
 from their special remote and sent to the attacker's git-annex repository.
 
-Encrypted git-annex repositories are not vulnerable to these attacks.
-
 ## git-annex security fixes
 
 To avoid these attacks, after autoenabling a special remote, git-annex

security advisory
diff --git a/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
new file mode 100644
index 0000000000..5395fe4573
--- /dev/null
+++ b/doc/security/cross_repository_data_exfiltration_via_special_remote.mdwn
@@ -0,0 +1,47 @@
+git-annex was vulnerable to a private data exfiltration attack. Joey Hess
+discovered this attack.
+
+These security holes were fixed in git-annex (FIXME: version)
+
+## attack descriptions
+
+To perform these attacks, the attacker needs to have control over one of
+the remotes of the victim's git-annex repository. For example, they may
+provide a git-annex repository that the victim clones.
+
+The attacker also needs to have information about another git-annex 
+repository owned by the victim. This could be a published git repository,
+with annexed file contents that the victim is keeping private.
+
+In the simplest form of the attack, the attacker provides a git-annex
+repository that the victim clones. When git-annex is initialized in the
+victim's clone, it autoenables a special remote that the attacker has
+configured to access the victim's other, private git-annex repository.
+When the victim runs a command such as `git-annex sync --content`,
+their private files are retrieved from the special remote and sent to the
+attacker's git-annex repository.
+
+In another form of the attack, autoenable of special remotes is not needed.
+Instead, the attacker provides a special remote (eg a S3 bucket) along with
+their git-annex repository. The attacker waits for the victim to clone the
+repository and manually enable and use their special remote. Then they update
+the configuration of the special remote, retargeting it to access the victim's
+own special remote (eg the victim's private S3 bucket). When the victim runs a
+command such as `git-annex sync --content`, their private files are retrieved
+from their special remote and sent to the attacker's git-annex repository.
+
+Encrypted git-annex repositories are not vulnerable to these attacks.
+
+## git-annex security fixes
+
+To avoid these attacks, after autoenabling a special remote, git-annex
+will refuse to send annexed files to remotes, and will also block git
+pushes, until the user verifies that the configuration of the autoenabled
+special remote is safe.
+
+Also, `git-annex enableremote` will display the configuration of the
+special remote so that the user can verify if it is safe.
+
+Additionally, changes to the configuration of special remotes that
+are received via the git-annex branch will not take effect until the user
+has verified they are safe.

updates
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
index a4a78506cc..c67538616d 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
@@ -31,11 +31,4 @@ The UI for this could look something like:
 What should verifyremote --unsafe do? At that point files can
 have already been gotten from it. Deleting the whole local repository
 might be a good move. It would need to advise the user about what to do.
-
-This fix is incomplete in that it still lets the attacker
-learn if I do have a given key in a location that they hypothesized.
-`git-annex sync` will get it from the autoenabled special remote,
-and update location tracking to say it's in my clone of the repository,
-and pushes the git-annex branch to origin, exposing that information
-to the attacker.
 """]]
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
index 0013df0d29..336a031c7c 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
@@ -21,7 +21,8 @@ security hole.
 One fix is for enabling a special remote to copy all security relevant
 parts of the remote config to git config settings, and have the special
 remote read those git config settings on use. Some special remotes
-already do that, but most don't, and there's no disciplined way to do it.
+already do that (eg rsync), but most don't, and there's no disciplined
+way to do it.
 
 It should be possible to fix this without modifying all the special remotes
 to do that. git-annex could remember that a special remote was enabled with
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment
index 75d82d6099..fb5037072c 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment
@@ -8,4 +8,14 @@ enableremote` the remote, and so avoid any autoenable specific security
 guardrails.
 
 That suggests that enableremote needs the same kind of security guardrails.
+That could be the same workflow with `git-annex verifyremote` needed after
+enableremote. Or if could be built in to it, and rely on the user reading
+to review it for safety:
+
+	> git-annex enableremote foo
+	enableremote foo
+		Enabling a special remote with this configuration:
+	                type=S3 bucket=foo
+		If this is not the remote repository you expected
+		to be using, run: git-annex verifyremote foo --unsafe
 """]]
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_5_706997b3faa77eed95d7d22d440c0b45._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_5_706997b3faa77eed95d7d22d440c0b45._comment
new file mode 100644
index 0000000000..e31f2cbd3a
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_5_706997b3faa77eed95d7d22d440c0b45._comment
@@ -0,0 +1,32 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2026-10-12T13:50:13Z"
+ content="""
+The fixes proposed above are incomplete in that an attacker can still learn
+if I have a given key in given location.
+
+`git-annex sync` will get it from the autoenabled special remote,
+and update location tracking to say it's in my clone of the repository,
+and pushes the git-annex branch to origin, exposing that information
+to the attacker.
+
+To prevent this, two options:
+
+1. Have a pre-push hook that prevents all pushes when there is an
+   autoenabled special remote that has not been verified.
+2. Avoid committing the journal to the git-annex branch before an
+   autoenabled special remote is verified.
+
+The pre-push hook would need to be installed before autoenabling a special
+remote. (If there's an existing pre-push hook that the user has installed
+that doesn't call git-annex, the autoenabling would need to fail. This
+would be an uncommon special case, since usually autoenabling happens at
+first git-annex init, when the repository is young. Also use of pre-push
+hooks is probably rare.)
+
+Avoiding commiting the journal to the git-annex branch would be easier to
+implement, but leaves the user with no indication why the git-annex branch
+is not getting sent to remotes as usual. The pre-push hook can display a
+useful message.
+"""]]

todo
diff --git a/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
new file mode 100644
index 0000000000..dffd108531
--- /dev/null
+++ b/doc/todo/arm64-ancient_bullseye_out_of_security_support.mdwn
@@ -0,0 +1,10 @@
+The arm64-ancient build uses debian bullseye, which has reached the end of
+security support.
+
+This build should either be switched to the current debian LTS, or stop
+being made. Since it targeted kernel 4.19 for android phones that still
+used that, it may either not be necessary any more, or the new debian LTS
+may not work with that kernel.
+
+It may be that, since termux now includes a git-annex package, it's not
+worth doing a build just for old android phones any longer. --[[Joey]]

Added a comment: Thanks!
diff --git a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_7_a518742fb8158d8b355205809c751dca._comment b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_7_a518742fb8158d8b355205809c751dca._comment
new file mode 100644
index 0000000000..b695dfcb0d
--- /dev/null
+++ b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_7_a518742fb8158d8b355205809c751dca._comment
@@ -0,0 +1,23 @@
+[[!comment format=mdwn
+ username="ewen"
+ avatar="http://cdn.libravatar.org/avatar/605b2981cb52b4af268455dee7a4f64e"
+ subject="Thanks!"
+ date="2026-10-09T21:32:27Z"
+ content="""
+Thanks for the link to the macOS standalone build workflow -- as you say it looks like `stack-botan.yaml` with `magicmime` enabled, and using Homebrew to supply some of the dependencies.  That's a useful reference for the future.
+
+Based on what I found yesterday I'm fairly sure both of:
+
+- Intel CPU (of a relevant vintage)
+- built with *exactly* Crypton 2.0.0 (ie, using `stack.yaml`, allowing Crypton 2.x.x *and* a restriction preventing Crypton 2.0.1 / Crypton 2.1.0)
+
+are required to trigger the build that crashes.
+
+Since Crypton 2.0.1 came out fairly soon after Crypton 2.0.0 and has the fix for the Intel CPU feature detection, I suspect that's an uncommon build configuration.  Likely only affecting Homebrew macOS Intel (\"Tier 3\") and Homebrew Linux Intel (\"Tier 2\" from memory), due to their (apparently mistaken) choice to block Crypton 2.0.1 in the dependency resolver.
+
+So I think this issue could be closed now, and just serve as future reference for other possible crash isues.
+
+I also agree with you it's quite hard to avoid dependencies-of-dependencies (especially recursively or where fairly key dependencies use the library/versions one would prefer to avoid).  Hopefully Crypon 2.x.x settles down a bit more now the clear \"major version\" breaking changes are done, because it looks hard to completely avoid in a network-related project.
+
+Ewen
+"""]]

comment
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment
new file mode 100644
index 0000000000..75d82d6099
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_4_a09886f2b1be0cba689db41d36953668._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 4"""
+ date="2026-10-09T18:16:22Z"
+ content="""
+The attacker could also document that the user ought to `git-annex
+enableremote` the remote, and so avoid any autoenable specific security
+guardrails.
+
+That suggests that enableremote needs the same kind of security guardrails.
+"""]]

update
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
index 0bb0e6207d..a4a78506cc 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
@@ -13,25 +13,29 @@ The UI for this could look something like:
 	[...]
 	(autoenabling special remote "foo")
 	[...]
-	> git-annex copy --to origin
-	git-annex: Error, sending files to special remotes is disabled
-	because the recently autoenabled special remote "foo" has not been
-	verified to be safe. Please examine the configuration of this
-	special remote and make sure it is not accessing any resource that
-	you would not expect it to access. If all looks well,
-	run "git-annex verified-safe foo"
+	get somefile (from foo) ok
+	copy somefile (to origin)
+	git-annex: Error, sending files to all remotes is disabled because
+	the recently autoenabled special remote "foo" has not been verified
+	to be safe. Please run "git-annex verifyremote foo" to do that.
+	push origin ok
 
-That relies on the configuration of special remotes being something that
-a) the user can verify to be safe and b) won't change out from under them
-in a subsequent `git pull` of the git-annex branch. Unfortunately, that is
-not actually the case for all of them. Eg for a S3 bucket the bucket= field
-in the remote config can be changed by a later git pull. It doesn't use 
-git config like eg the directory special remote does. So this might also
-need fixes to lots of special remotes.
+	> git-annex verifyremote foo
+	This is an automatically enabled special remote, with
+	this configuration:
+		type=S3 bucket=foo
+	Is this remote repository one you expected to be using?
+	If so, run:  git-annex verifyremote foo --safe
+	If not, run: git-annex verifyremote foo --unsafe
 
-The other problem with this fix is that it still lets the attacker
+What should verifyremote --unsafe do? At that point files can
+have already been gotten from it. Deleting the whole local repository
+might be a good move. It would need to advise the user about what to do.
+
+This fix is incomplete in that it still lets the attacker
 learn if I do have a given key in a location that they hypothesized.
 `git-annex sync` will get it from the autoenabled special remote,
-and update location tracking to say it's in my clone of the repository.
-Then any `git push origin` will expose that information to the attacker.
+and update location tracking to say it's in my clone of the repository,
+and pushes the git-annex branch to origin, exposing that information
+to the attacker.
 """]]
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
index ba602d9e9a..0013df0d29 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
@@ -20,7 +20,8 @@ security hole.
 
 One fix is for enabling a special remote to copy all security relevant
 parts of the remote config to git config settings, and have the special
-remote read those git config settings on use.
+remote read those git config settings on use. Some special remotes
+already do that, but most don't, and there's no disciplined way to do it.
 
 It should be possible to fix this without modifying all the special remotes
 to do that. git-annex could remember that a special remote was enabled with

update
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
index 539cd6dd3c..c399f0fb95 100644
--- a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -1,7 +1,7 @@
 An git-annex repository with an autoenable=yes special remote can be used
 to exfiltrate data from another git-annex repository.
 
-For example, suppose that ~/foo on my laptop is a git-annex repository
+For example, suppose that ~/annex on my laptop is a git-annex repository
 that contains some annexed files whose content I only provide to some
 people. But I am not trying to hide that I have those files in that
 repository. So I push the git repository, without the annexed files,
@@ -10,23 +10,32 @@ to a public server.
 All an attacker needs to do is make a git-annex repository that contains an
 autoenabled special remote of type git:
 
-    git-annex initremote foo type=git location=/home/joey/foo autoenable=true
+    git-annex initremote foo type=git location=/home/joey/annex autoenable=true
 
-(They will first need to dummy up their own ~/foo repository, with the same
-uuid as mine; that uuid is discoverable in what I pushed.)
+(They will first need to dummy up their own /home/joey/annex repository,
+with the same uuid as mine; that uuid is discoverable in what I pushed.)
 
 In their repository, they can make some annexed files, with innocuous
 names, that use the keys of the annexed files I am keeping private.
+And make the git-annex branch record that those keys are located in the
+uuid of my repository.
 
 Then they only need to coax me into cloning their repository and running
 git-annex sync. The foo remote is autoenabled and sync will get my private
-files from foo. And, if the attacker-controlled origin has preferred
-content configured, will send them on to their repository.
+files from ~/annex. And, if the attacker's repository has preferred
+content configured, will send them on to origin.
 
 This security hole is not limited to autoenabled git remotes. It could be
 an autoenabled S3 bucket, if I habitually keep S3 environment variables
 set. Or it could be an autoenabled rsync special remote with an rsyncurl
-that points into my server via ssh.
+that points into my server via ssh. Unlike the git special remote,
+some of those don't need the UUID to match either.
+
+A harder to exploit variant of this security hole is when I have not
+published my ~/annex git repository. The attacker guesses that I am in
+possession of a file with a known hash, and guesses where I might be
+keeping it. They can try many locations with many autoenabled special
+remotes.
 
 (It's almost possible to use this to exfiltrate arbitrary non-annexed files.
 The attacker could make a directory or rsync special remote with
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
new file mode 100644
index 0000000000..0bb0e6207d
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_2_6d91ed406f1594e5d4261728128250df._comment
@@ -0,0 +1,37 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2026-10-09T16:36:22Z"
+ content="""
+A way to fix this while keeping autoenable could be to make git-annex
+refuse to push annexed files to remotes by default after autoenabling a
+special remote. 
+
+The UI for this could look something like:
+
+	> git-annex sync
+	[...]
+	(autoenabling special remote "foo")
+	[...]
+	> git-annex copy --to origin
+	git-annex: Error, sending files to special remotes is disabled
+	because the recently autoenabled special remote "foo" has not been
+	verified to be safe. Please examine the configuration of this
+	special remote and make sure it is not accessing any resource that
+	you would not expect it to access. If all looks well,
+	run "git-annex verified-safe foo"
+
+That relies on the configuration of special remotes being something that
+a) the user can verify to be safe and b) won't change out from under them
+in a subsequent `git pull` of the git-annex branch. Unfortunately, that is
+not actually the case for all of them. Eg for a S3 bucket the bucket= field
+in the remote config can be changed by a later git pull. It doesn't use 
+git config like eg the directory special remote does. So this might also
+need fixes to lots of special remotes.
+
+The other problem with this fix is that it still lets the attacker
+learn if I do have a given key in a location that they hypothesized.
+`git-annex sync` will get it from the autoenabled special remote,
+and update location tracking to say it's in my clone of the repository.
+Then any `git push origin` will expose that information to the attacker.
+"""]]
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
new file mode 100644
index 0000000000..ba602d9e9a
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_3_f15bb857b5201794c4987dbc9e9dde6c._comment
@@ -0,0 +1,40 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2026-10-09T16:57:43Z"
+ content="""
+Previous comment go me thinking, this security hole may not need an
+autoenabled special remote at all.
+
+For example, if the attacker knows that I am storing annexed objects in a
+private S3 bucket, they can make a git-annex repository with a special
+remote for a S3 bucket that contains files they know I want. When I clone
+it, I can manually enable that special remote and use it. And then the
+attacker strikes, by updating the git-annex branch remote.log so that
+the special remote now points to my own private S3 bucket. Causing 
+`git-annex sync` to exfiltrate annex objects from it.
+
+This is really a separate security hole, but fixing it comprehensively
+across special remotes seems to be needed in order to fix the autoenable
+security hole.
+
+One fix is for enabling a special remote to copy all security relevant
+parts of the remote config to git config settings, and have the special
+remote read those git config settings on use.
+
+It should be possible to fix this without modifying all the special remotes
+to do that. git-annex could remember that a special remote was enabled with
+a particular special remote config, and supply that config to it, even
+after the git-annex branch was updated with a newer config.
+
+To handle legitimate config changes, git-annex could notify the user about
+a config change and have them verify it before using it.
+
+How to handle upgrades, where it's not yet recorded which config a special
+remote was enabled with? It could default to the first or the last
+remote.log config of the special remote. First would be undesirable when
+git-annex enableremote has been used in the past to change the config
+in some signficant way. Last risks exposing the user of an upgraded
+git-annex to this security hole, but only if an attacker has already
+updated the git-annex branch.
+"""]]

comment
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_1_ef99b347533207ac6ef7f3a51757d3ba._comment b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_1_ef99b347533207ac6ef7f3a51757d3ba._comment
new file mode 100644
index 0000000000..30445818fd
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote/comment_1_ef99b347533207ac6ef7f3a51757d3ba._comment
@@ -0,0 +1,19 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-09T15:23:02Z"
+ content="""
+The obvious fixes for this are to not autoenable anything,
+or to somehow prompt the user to verify autoenabled special
+remotes before use.
+
+I know that autoenabled special remotes are broadly used in the datalad
+community, and losing them would probably break a great many repository
+workflows.
+
+Prompting the user is difficult because autoenabling can happen on the
+first git-annex use, which could be expected to legitimately get files from
+the autoenabled special remote.
+
+I'm struggling to think of other ways to fix this though.
+"""]]

damn
diff --git a/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
new file mode 100644
index 0000000000..539cd6dd3c
--- /dev/null
+++ b/doc/bugs/security_hole_data_exfiltration_via_autoenabled_remote.mdwn
@@ -0,0 +1,40 @@
+An git-annex repository with an autoenable=yes special remote can be used
+to exfiltrate data from another git-annex repository.
+
+For example, suppose that ~/foo on my laptop is a git-annex repository
+that contains some annexed files whose content I only provide to some
+people. But I am not trying to hide that I have those files in that
+repository. So I push the git repository, without the annexed files,
+to a public server.
+
+All an attacker needs to do is make a git-annex repository that contains an
+autoenabled special remote of type git:
+
+    git-annex initremote foo type=git location=/home/joey/foo autoenable=true
+
+(They will first need to dummy up their own ~/foo repository, with the same
+uuid as mine; that uuid is discoverable in what I pushed.)
+
+In their repository, they can make some annexed files, with innocuous
+names, that use the keys of the annexed files I am keeping private.
+
+Then they only need to coax me into cloning their repository and running
+git-annex sync. The foo remote is autoenabled and sync will get my private
+files from foo. And, if the attacker-controlled origin has preferred
+content configured, will send them on to their repository.
+
+This security hole is not limited to autoenabled git remotes. It could be
+an autoenabled S3 bucket, if I habitually keep S3 environment variables
+set. Or it could be an autoenabled rsync special remote with an rsyncurl
+that points into my server via ssh.
+
+(It's almost possible to use this to exfiltrate arbitrary non-annexed files.
+The attacker could make a directory or rsync special remote with
+importtree=yes that autoenables. The only reason this doesn't work
+is that remote.name.annex-tracking-branch needs to be set in order
+to make `git-annex sync` use it. They could try to social engineer 
+me into running `git-annex import`, but that seems considerably harder.
+Still, while this seems too hard to be a viable attack, it seems likely
+that any fix to the main attack will also close off this one.) 
+
+--[[Joey]]

document rsyncurl in a way that leaves open local destination paths
While not generally a reason to reach for the rsync special remote, it
makes sense that they work.
diff --git a/doc/bugs/rsyncurl_without___34____58____34___creates_local_folder_as_remote/comment_3_da5d2210e10eca9a13bd786670e22c75._comment b/doc/bugs/rsyncurl_without___34____58____34___creates_local_folder_as_remote/comment_3_da5d2210e10eca9a13bd786670e22c75._comment
new file mode 100644
index 0000000000..e82a0902ac
--- /dev/null
+++ b/doc/bugs/rsyncurl_without___34____58____34___creates_local_folder_as_remote/comment_3_da5d2210e10eca9a13bd786670e22c75._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2026-10-09T14:57:29Z"
+ content="""
+I think it's expected behavior. I've documented it.
+"""]]
diff --git a/doc/special_remotes/rsync.mdwn b/doc/special_remotes/rsync.mdwn
index ad46fab75f..65eae80ac4 100644
--- a/doc/special_remotes/rsync.mdwn
+++ b/doc/special_remotes/rsync.mdwn
@@ -14,8 +14,9 @@ Or for using rsync over SSH
 
 These parameters can be passed to `git annex initremote` to configure rsync:
 
-* `rsyncurl` - Required. This is the url or `hostname:/directory` to 
-  pass to rsync to tell it where to store content.
+* `rsyncurl` - Required. This is the passed to rsync to tell it
+  where to store content. It can be an url or `hostname:/directory`;
+  see the rsync documentation for details about how it handles destinations.
 
 * `encryption` - One of "none", "hybrid", "shared", or "pubkey".
   See [[encryption]].

comment
diff --git a/doc/bugs/git-remote-annex_for_directory_on_macOS/comment_2_788505653b522dc1435888e9aafbaca5._comment b/doc/bugs/git-remote-annex_for_directory_on_macOS/comment_2_788505653b522dc1435888e9aafbaca5._comment
new file mode 100644
index 0000000000..579f3f0fee
--- /dev/null
+++ b/doc/bugs/git-remote-annex_for_directory_on_macOS/comment_2_788505653b522dc1435888e9aafbaca5._comment
@@ -0,0 +1,29 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2026-10-09T14:44:21Z"
+ content="""
+What won't work for such a directory special remote is cloning it from the
+url that git-annex outputs when pushing to it. Eg:
+
+	joey@darkstar:~/tmp/bb/repo>git push local1 master
+	Full remote url: annex::23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory
+	Everything up-to-date
+	joey@darkstar:~/tmp/bb/repo>..
+	cd -- ..
+	joey@darkstar:~/tmp/bb>git clone 'annex::23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory'
+	Cloning into '23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory'...
+	git-remote-annex: Specify directory=
+	
+	fatal: remote helper 'annex' aborted session
+	- exit 128
+	joey@darkstar:~/tmp/bb>git clone 'annex::23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory&directory=specialdirectoryremote'
+	Cloning into '23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory&directory=specialdirectoryremote'...
+
+Working around that is possible, but you have to add the directory parameter to the url:
+
+	joey@darkstar:~/tmp/bb>git clone 'annex::23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory&directory=specialdirectoryremote'
+	Cloning into '23158cb0-7aa5-43ea-9ca3-c8254e9aa1f0?encryption=none&type=directory&directory=specialdirectoryremote'...
+
+This poor fit is why I say that a bare git repository is better.
+"""]]

close
diff --git a/doc/bugs/git-remote-annex_for_directory_on_macOS.mdwn b/doc/bugs/git-remote-annex_for_directory_on_macOS.mdwn
index 435fce443d..80b99c9c9e 100644
--- a/doc/bugs/git-remote-annex_for_directory_on_macOS.mdwn
+++ b/doc/bugs/git-remote-annex_for_directory_on_macOS.mdwn
@@ -360,3 +360,4 @@ ok
 ### 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)
 Yup!
 
+> not a bug, [[done]] --[[Joey]]
diff --git a/doc/bugs/git-remote-annex_for_directory_on_macOS/comment_1_80ad795e8b808b43fb0d919743ed9cbe._comment b/doc/bugs/git-remote-annex_for_directory_on_macOS/comment_1_80ad795e8b808b43fb0d919743ed9cbe._comment
new file mode 100644
index 0000000000..ccd739194d
--- /dev/null
+++ b/doc/bugs/git-remote-annex_for_directory_on_macOS/comment_1_80ad795e8b808b43fb0d919743ed9cbe._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-09T14:36:29Z"
+ content="""
+This is not macOS specific. The directory special remote requires a
+directory= parameter be given to initremote. That is to support things like
+a directory special remote that points to a USB drive that moves between
+several computers. The mount point of the drive can be different on each
+computer, and so each place the directory special remote is enabled it
+needs the directory= to be specified.
+
+I don't really see any point in using git-remote-annex with a directory
+special remote. You can just as well use a bare git repository. 
+
+But anyway, if you pass the directory= parameter to enableremote, it does work.
+
+BTW, it's not necessary to run enableremote after initremote. You can just
+run initremote --with-url.
+"""]]

close
diff --git a/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4.mdwn b/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4.mdwn
index 9bad084bec..c00dfe51f3 100644
--- a/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4.mdwn
+++ b/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4.mdwn
@@ -44,3 +44,5 @@ Android 9. LineageOS.
 So, I've been trying to uss it for years. Kind of like how I tried using Emacs and went bankrupt twice before things clicked. I was so hung up on the symlinks. Then I finally understood some things, thanks Gemini, and now I'm using annex.addunlocked and annex.thin and I feel really nerdy and cool. I would love to get this working on my tablet too.
 
 Thanks!
+
+> [[done]]; use termux git-annex package --[[Joey]]
diff --git a/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4/comment_1_190da665e1a45eb1704ca39bceeb4579._comment b/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4/comment_1_190da665e1a45eb1704ca39bceeb4579._comment
new file mode 100644
index 0000000000..0c39e21649
--- /dev/null
+++ b/doc/bugs/proot_info__58___vpid_1__58___terminated_with_signal_4/comment_1_190da665e1a45eb1704ca39bceeb4579._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2026-10-09T14:32:29Z"
+ content="""
+termux now includes a git-annex package, and that avoids the proot hack
+that the git-annex.linux standalone build uses to run on android.
+
+So, I think that would be the solution to this problem. 
+
+Since using the termux package is now the recommended way to install
+git-annex on android, and whatever the problem with proot is unlikely to
+get resolved, I'm going to close this.
+"""]]

comment
diff --git a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_6_45ee404e7aac0beb6583fc167e8d761e._comment b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_6_45ee404e7aac0beb6583fc167e8d761e._comment
new file mode 100644
index 0000000000..f798b55c8c
--- /dev/null
+++ b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_6_45ee404e7aac0beb6583fc167e8d761e._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 6"""
+ date="2026-10-09T14:04:05Z"
+ content="""
+It's deeply unfortunate that crypton has stated being developed by pointing
+an LLM at it and hoping that the (not well understood) results work. The
+consequences can be seen in the crypton changelog.
+
+git-annex is migrating away from crypton to the extent it can, but
+unfortunately it's used for https libraries and I have not found a
+way around that.
+
+PS, the build environment used for the MacOs standalone build 
+is [here](https://github.com/joeyh/gitannexbuilder/blob/github/.github/workflows/build-macos-intel.yaml)
+but is basically just the stack-botan.yaml. That pulls in crypton-1.0.6
+currently.
+"""]]

Added a comment: crypton 2.0.0 known issue on Intel CPUs (fixed in crypton 2.0.1, but Homebrew Formula blocks 2.0.1)
diff --git a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_5_8f8da7f67ef306b5eabcef96695079a0._comment b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_5_8f8da7f67ef306b5eabcef96695079a0._comment
new file mode 100644
index 0000000000..f743e4456e
--- /dev/null
+++ b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_5_8f8da7f67ef306b5eabcef96695079a0._comment
@@ -0,0 +1,76 @@
+[[!comment format=mdwn
+ username="ewen"
+ avatar="http://cdn.libravatar.org/avatar/605b2981cb52b4af268455dee7a4f64e"
+ subject="crypton 2.0.0 known issue on Intel CPUs (fixed in crypton 2.0.1, but Homebrew Formula blocks 2.0.1)"
+ date="2026-10-09T06:14:12Z"
+ content="""
+Apologies for the many comments to get here, but I suspect I've found the cause at last, in the [Crypton 2.0.1 release notes](https://hackage.haskell.org/package/crypton-2.0.1/changelog):
+
+> fix(cpu): stop reading Intel's SDBG bit as AMD's XOP, which crashed SHA-512 and ChaCha20 on Broadwell and later.
+
+which links to this [bug report on Crypton 2.0.0](https://github.com/kazu-yamamoto/crypton/issues/202) with similar crashes, that starts with:
+
+> Following the 2.0.0 bump I get SIGILL on an elder machine (i7-6700K / Skylake).
+
+Where on macOS, signal 4 is `ILL`:
+
+```
+ewen@basadi:~$ kill -l 4
+ILL
+ewen@basadi:~$ 
+```
+
+so the symptoms match.
+
+And the affected macOS Intel system (Intel Mac Mini, from 2020) I suspect is roughly the same CPU generation to be affected.
+
+So in theory rather than:
+
+```
+ewen@basadi:~$ git annex version | grep crypton
+dependency versions: aws-0.25.3 bloomfilter-2.0.1.3 crypton-2.0.0 DAV-1.3.4 feed-1.3.2.1 ghc-9.14.1 http-client-0.7.19 torrent-10000.1.3 uuid-1.3.16.1 yesod-1.6.2.3
+ewen@basadi:~$ 
+```
+
+I actually need the build to be against `crypton-2.0.1` on macOS Intel.... but the Homebrew Formula explicitly blocks that for another reason:
+
+```
+      # https://github.com/psibi/crypton-conduit/issues/5
+      \"--constraint=crypton<2.0.1\",
+```
+
+where [https://github.com/psibi/crypton-conduit/issues/5](https://github.com/psibi/crypton-conduit/issues/5) was one of the issues I noted above as a new change in the Homebrew Formula, which suggests that `git-annex` won't build (on macOS) with `crypton` 2.0.1.
+
+However I think they're confused about [what changed in `crypton` 2.0.1](https://hackage.haskell.org/package/crypton-2.0.1/changelog) (which is basically a one line fix) and are [probably thinking of `crypton` 2.1.0](https://hackage.haskell.org/package/crypton-2.1.0/changelog) (which is a much larger change), and I've [said so in the crypton-conduit issue report](https://github.com/psibi/crypton-conduit/issues/5#issuecomment-6075337746), where they noted the conflict.
+
+After forcing Homebrew locally to build with:
+
+```
+cabal v2-install --allow-newer=base,template-haskell --constraint=crypton<2.1.0 --constraint=magic<2 --constraint=QuickCheck<2.17 --constraint=persistent-sqlite +systemlib +use-pkgconfig
+```
+
+which pulls in `crypton` 2.0.1:
+
+```
+ewen@basadi:~$ git annex version | egrep 'crypton|git-annex version'
+git-annex version: 10.20261006
+dependency versions: aws-0.25.3 bloomfilter-2.0.1.3 crypton-2.0.1 DAV-1.3.4 feed-1.3.2.1 ghc-9.14.1 http-client-0.7.19 torrent-10000.1.3 uuid-1.3.16.1 yesod-1.6.2.3
+ewen@basadi:~$ 
+```
+
+I find that `git annex addurl` works again:
+
+```
+ewen@basadi:/tmp/test-annex$ git annex addurl --pathdepth -1 'https://download.2600.com/mediadownload/www.2600.com/offthewall/mp3files/2026/off_the_wall__20261006-128.mp3'
+addurl https://download.2600.com/mediadownload/www.2600.com/offthewall/mp3files/2026/off_the_wall__20261006-128.mp3 
+(to off_the_wall__20261006-128.mp3) ok
+(recording state in git...)
+ewen@basadi:/tmp/test-annex$
+```
+
+and I assume that `git annex importfeed` will probably work again too.
+
+Ewen
+
+PS: The working Homebrew bottle install on macOS ARM also has `crypton-2.0.0` as the build dependency, but since it seems to be an Intel CPU specific bug in Crypton 2.0.0, that wouldn't matter on ARM.
+"""]]

Added a comment: git-annex depends on ram now (10.20261006)
diff --git a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_4_c41def6d91e367c83afd9e0cd9257874._comment b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_4_c41def6d91e367c83afd9e0cd9257874._comment
new file mode 100644
index 0000000000..7bcdb1dd78
--- /dev/null
+++ b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_4_c41def6d91e367c83afd9e0cd9257874._comment
@@ -0,0 +1,27 @@
+[[!comment format=mdwn
+ username="ewen"
+ avatar="http://cdn.libravatar.org/avatar/605b2981cb52b4af268455dee7a4f64e"
+ subject="git-annex depends on ram now (10.20261006)"
+ date="2026-10-09T05:03:11Z"
+ content="""
+Ah, on closer inspection of the changes it looks like git-annex itself now depends on `ram` (from `stack.yaml`):
+
+```
+[...]
+resolver: lts-24.52
+extra-deps:
+- aws-0.25.2
+- file-io-0.2.0
+- blake3-0.3
+- magic-1.1
+- ram-0.22.0
+```
+
+which is presumably at least part of the reason why HomeBrew went with the alternative approach of depending on an unmerged PR of `yesod-static`.
+
+Unfortunately the Homebrew git-annex Formula PR (for 10.20261005) -- [https://github.com/Homebrew/homebrew-core/pull/315642/files](https://github.com/Homebrew/homebrew-core/pull/315642/files) -- doesn't give any commentary on why the changes were made, and the working source branch is naturally deleted.
+
+But building `git-annex` 10.20261005 / 10.20261006 with the old `ram < 0` build constraint from the Homebrew Formula seems unlikely to work.
+
+Ewen
+"""]]

Added a comment: Homebrew Formula adopts yesod-static PR 1916 AFAICT
diff --git a/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_3_764ce2edb9bb713167da3e1204176a56._comment b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_3_764ce2edb9bb713167da3e1204176a56._comment
new file mode 100644
index 0000000000..b91056dfed
--- /dev/null
+++ b/doc/bugs/10.20261006__58___addurl__58___git-annex_died_of_signal_4/comment_3_764ce2edb9bb713167da3e1204176a56._comment
@@ -0,0 +1,22 @@
+[[!comment format=mdwn
+ username="ewen"
+ avatar="http://cdn.libravatar.org/avatar/605b2981cb52b4af268455dee7a4f64e"
+ subject="Homebrew Formula adopts yesod-static PR 1916 AFAICT"
+ date="2026-10-09T02:56:45Z"
+ content="""
+Looking closer, as best I can tell Homebrew have effectively proactively adopted the PR with their recent Formula change (above), since the URL:
+
+https://github.com/yesodweb/yesod/archive/23f8d636842023c7cde36109ea24258df0d5ecd6.tar.gz
+
+seems to correspond to this commit:
+
+[https://github.com/leftaroundabout/yesod/commit/23f8d636842023c7cde36109ea24258df0d5ecd6](https://github.com/leftaroundabout/yesod/commit/23f8d636842023c7cde36109ea24258df0d5ecd6)
+
+at the tip of the source branch of the PR ([unmerged yesod PR #1916](https://github.com/yesodweb/yesod/pull/1916)).  Even though the download is via the upstream gitrepo.
+
+For the record, I'd report this to Homebrew but they (a) officially don't support macOS Intel any longer (\"Tier 3\" as of last month), and (b) officially don't support local builds).
+
+It is however seeming like I should possibly try building 10.20261006 *without* this pinning to a PR version of `yesod-static`, as the more I look at that change in the Homebrew Formula, the more surprising it seems.
+
+Ewen
+"""]]