diff --git a/Annex/CatFile.hs b/Annex/CatFile.hs
--- a/Annex/CatFile.hs
+++ b/Annex/CatFile.hs
@@ -9,8 +9,9 @@
 	catFile,
 	catObject,
 	catObjectDetails,
+	catFileHandle,
 	catKey,
-	catFileHandle
+	catKeyFile,
 ) where
 
 import qualified Data.ByteString.Lazy as L
@@ -46,4 +47,16 @@
 
 {- From the Sha or Ref of a symlink back to the key. -}
 catKey :: Ref -> Annex (Maybe Key)
-catKey ref = fileKey . takeFileName . encodeW8 . L.unpack  <$> catObject ref
+catKey ref = do
+	l <- encodeW8 . L.unpack  <$> catObject ref
+	return $ if isLinkToAnnex l
+		then fileKey $ takeFileName l
+		else Nothing
+
+{- From a file in git back to the key.
+ -
+ - Prefixing the file with ./ makes this work even if in a subdirectory
+ - of a repo. For some reason, HEAD is sometimes needed.
+ -}
+catKeyFile :: FilePath -> Annex (Maybe Key)
+catKeyFile f = catKey $ Ref $ "HEAD:./" ++ f
diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -309,7 +309,6 @@
   where
 	goindirect = indirect =<< inRepo (gitAnnexLocation key)
 
-
 cleanObjectLoc :: Key -> Annex ()
 cleanObjectLoc key = do
 	file <- inRepo $ gitAnnexLocation key
diff --git a/Annex/Direct.hs b/Annex/Direct.hs
--- a/Annex/Direct.hs
+++ b/Annex/Direct.hs
@@ -55,16 +55,7 @@
 			(Just key, Nothing, _) -> deletedannexed file key
 			(Nothing, Nothing, _) -> deletegit file
 			(_, Just _, _) -> addgit file
-	go (file, Nothing) = do
-		mstat <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file
-		case (mstat, toCache =<< mstat) of
-			(Nothing, _) -> noop
-			(Just stat, Just cache)
-				| isSymbolicLink stat -> addgit file
-				| otherwise -> void $ addDirect file cache
-			(Just stat, Nothing)
-				| isSymbolicLink stat -> addgit file
-				| otherwise -> noop
+	go _ = noop
 
 	modifiedannexed file oldkey cache = do
 		void $ removeAssociatedFile oldkey file
@@ -190,10 +181,13 @@
 				liftIO $ replaceFile f $ moveFile loc
 			, return Nothing
 			)
-		(loc':_) -> return $ Just $ do
+		(loc':_) -> ifM (liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f)
 			{- Another direct file has the content, so
 			 - hard link to it. -}
-			liftIO $ replaceFile f $ createLink loc'
+			( return $ Just $ do
+				liftIO $ replaceFile f $ createLink loc'
+			, return Nothing
+			)
 
 {- Removes a direct mode file, while retaining its content. -}
 removeDirect :: Key -> FilePath -> Annex ()
diff --git a/Assistant.hs b/Assistant.hs
--- a/Assistant.hs
+++ b/Assistant.hs
@@ -151,6 +151,7 @@
 #else
 #warning Building without the webapp. You probably need to install Yesod..
 #endif
+import Assistant.Environment
 import qualified Utility.Daemon
 import Utility.LogFile
 import Utility.ThreadScheduler
@@ -178,6 +179,7 @@
 startAssistant :: Bool -> (IO () -> IO ()) -> Maybe (String -> FilePath -> IO ()) -> Annex ()
 startAssistant assistant daemonize webappwaiter = withThreadState $ \st -> do
 	checkCanWatch
+	when assistant $ checkEnvironment
 	dstatus <- startDaemonStatus
 	liftIO $ daemonize $
 		flip runAssistant go =<< newAssistantData st dstatus
diff --git a/Assistant/Environment.hs b/Assistant/Environment.hs
new file mode 100644
--- /dev/null
+++ b/Assistant/Environment.hs
@@ -0,0 +1,26 @@
+{- git-annex assistant environment
+ -
+ - Copyright 2012 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Assistant.Environment where
+
+import Assistant.Common
+import Utility.UserInfo
+import qualified Git.Config
+
+import System.Posix.Env
+
+{- Checks that the system's environment allows git to function.
+ - Git requires a GECOS username, or suitable git configuration, or
+ - environment variables. -}
+checkEnvironment :: Annex ()
+checkEnvironment = do
+	username <- liftIO myUserName
+	gecos <- liftIO myUserGecos
+	gitusername <- fromRepo $ Git.Config.getMaybe "user.name"
+	when (null gecos && (gitusername == Nothing || gitusername == Just "")) $
+		-- existing environment is not overwritten
+		liftIO $ setEnv "GIT_AUTHOR_NAME" username False
diff --git a/Assistant/Threads/Watcher.hs b/Assistant/Threads/Watcher.hs
--- a/Assistant/Threads/Watcher.hs
+++ b/Assistant/Threads/Watcher.hs
@@ -138,7 +138,7 @@
  - really been modified. -}
 onAddDirect :: Handler
 onAddDirect file fs = do
-	v <- liftAnnex $ catKey (Ref $ ':':file)
+	v <- liftAnnex $ catKeyFile file
 	case (v, fs) of
 		(Just key, Just filestatus) ->
 			ifM (liftAnnex $ changedFileStatus key filestatus)
@@ -158,11 +158,11 @@
   where
 	go (Just (key, _)) = do
 		link <- liftAnnex $ calcGitLink file key
-		ifM ((==) link <$> liftIO (readSymbolicLink file))
+		ifM ((==) (Just link) <$> liftIO (catchMaybeIO $ readSymbolicLink file))
 			( do
 				s <- getDaemonStatus
 				checkcontent key s
-				ensurestaged link s
+				ensurestaged (Just link) s
 			, do
 				liftIO $ removeFile file
 				liftIO $ createSymbolicLink link file
@@ -170,8 +170,8 @@
 				addlink link
 			)
 	go Nothing = do -- other symlink
-		link <- liftIO (readSymbolicLink file)
-		ensurestaged link =<< getDaemonStatus
+		mlink <- liftIO (catchMaybeIO $ readSymbolicLink file)
+		ensurestaged mlink =<< getDaemonStatus
 
 	{- This is often called on symlinks that are already
 	 - staged correctly. A symlink may have been deleted
@@ -184,12 +184,13 @@
 	 - (If the daemon has never ran before, avoid staging
 	 - links too.)
 	 -}
-	ensurestaged link daemonstatus
+	ensurestaged (Just link) daemonstatus
 		| scanComplete daemonstatus = addlink link
 		| otherwise = case filestatus of
 			Just s
 				| not (afterLastDaemonRun (statusChangeTime s) daemonstatus) -> noChange
 			_ -> addlink link
+	ensurestaged Nothing _ = noChange
 
 	{- For speed, tries to reuse the existing blob for symlink target. -}
 	addlink link = do
diff --git a/Backend.hs b/Backend.hs
--- a/Backend.hs
+++ b/Backend.hs
@@ -20,9 +20,11 @@
 import Common.Annex
 import qualified Annex
 import Annex.CheckAttr
+import Annex.CatFile
 import Types.Key
 import Types.KeySource
 import qualified Types.Backend as B
+import Config
 
 -- When adding a new backend, import it here and add it to the list.
 import qualified Backend.SHA
@@ -73,21 +75,32 @@
 		| otherwise = c
 
 {- Looks up the key and backend corresponding to an annexed file,
- - by examining what the file symlinks to. -}
+ - by examining what the file symlinks to.
+ -
+ - In direct mode, there is often no symlink on disk, in which case
+ - the symlink is looked up in git instead. However, a real symlink
+ - on disk still takes precedence over what was committed to git in direct
+ - mode.
+ -}
 lookupFile :: FilePath -> Annex (Maybe (Key, Backend))
 lookupFile file = do
 	tl <- liftIO $ tryIO $ readSymbolicLink file
 	case tl of
-		Left _ -> return Nothing
-		Right l -> makekey l
+		Right l
+			| isLinkToAnnex l -> makekey l
+			| otherwise -> return Nothing
+		Left _ -> ifM isDirect
+			( maybe (return Nothing) makeret =<< catKeyFile file
+			, return Nothing
+			)
   where
-	makekey l = maybe (return Nothing) (makeret l) (fileKey $ takeFileName l)
-	makeret l k = let bname = keyBackendName k in
+	makekey l = maybe (return Nothing) makeret (fileKey $ takeFileName l)
+	makeret k = let bname = keyBackendName k in
 		case maybeLookupBackendName bname of
 			Just backend -> do
 				return $ Just (k, backend)
 			Nothing -> do
-				when (isLinkToAnnex l) $ warning $
+				warning $
 					"skipping " ++ file ++
 					" (unknown backend " ++ bname ++ ")"
 				return Nothing
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,9 +1,18 @@
-git-annex (3.20130103) UNRELEASED; urgency=low
+git-annex (3.20130107) unstable; urgency=low
 
   * webapp: Add UI to stop and restart assistant.
   * committer: Fix a file handle leak.
+  * assistant: Make expensive transfer scan work fully in direct mode.
+  * More commands work in direct mode repositories: find, whereis, move, copy,
+    drop, log, fsck, add, addurl.
+  * sync: No longer automatically adds files in direct mode.
+  * assistant: Detect when system is not configured with a user name,
+    and set environment to prevent git from failing.
+  * direct: Avoid hardlinking symlinks that point to the same content
+    when the content is not present.
+  * Fix transferring files to special remotes in direct mode.
 
- -- Joey Hess <joeyh@debian.org>  Thu, 03 Jan 2013 14:58:45 -0400
+ -- Joey Hess <joeyh@debian.org>  Mon, 07 Jan 2013 01:01:41 -0400
 
 git-annex (3.20130102) unstable; urgency=low
 
diff --git a/Command/Add.hs b/Command/Add.hs
--- a/Command/Add.hs
+++ b/Command/Add.hs
@@ -21,14 +21,19 @@
 import Utility.Touch
 import Utility.FileMode
 import Config
+import qualified Git.HashObject
+import qualified Git.UpdateIndex
+import Git.Types
 
 def :: [Command]
-def = [notDirect $ notBareRepo $
-	command "add" paramPaths seek "add files to annex"]
+def = [notBareRepo $ command "add" paramPaths seek "add files to annex"]
 
 {- Add acts on both files not checked into git yet, and unlocked files. -}
 seek :: [CommandSeek]
-seek = [withFilesNotInGit start, withFilesUnlocked start]
+seek =
+	[ withFilesNotInGit start
+	, whenNotDirect $ withFilesUnlocked start
+	]
 
 {- The add subcommand annexes a file, generating a key for it using a
  - backend, and then moving it into the annex directory and setting up
@@ -60,15 +65,15 @@
 	tmp <- fromRepo gitAnnexTmpDir
 	createAnnexDirectory tmp
 	liftIO $ do
-		(tmpfile, handle) <- openTempFile tmp (takeFileName file)
-		hClose handle
+		(tmpfile, h) <- openTempFile tmp (takeFileName file)
+		hClose h
 		nukeFile tmpfile
 		createLink file tmpfile
 		return $ KeySource { keyFilename = file , contentLocation = tmpfile }
 
 {- Moves a locked down file into the annex.
  -
- - In direct mode, leaves the file alone, and just updates bookeeping
+ - In direct mode, leaves the file alone, and just updates bookkeeping
  - information.
  -}
 ingest :: KeySource -> Annex (Maybe Key)
@@ -146,11 +151,21 @@
 {- Note: Several other commands call this, and expect it to 
  - create the symlink and add it. -}
 cleanup :: FilePath -> Key -> Bool -> CommandCleanup
-cleanup file key hascontent = do
-	_ <- link file key hascontent
-	params <- ifM (Annex.getState Annex.force)
-		( return [Param "-f"]
-		, return []
-		)
-	Annex.Queue.addCommand "add" (params++[Param "--"]) [file]
-	return True
+cleanup file key hascontent = ifM (isDirect <&&> pure hascontent)
+	( do
+		l <- calcGitLink file key
+		sha <- inRepo $ Git.HashObject.hashObject BlobObject l
+		Annex.Queue.addUpdateIndex =<<
+			inRepo (Git.UpdateIndex.stageSymlink file sha)
+		when hascontent $
+			logStatus key InfoPresent
+		return True
+	, do
+		_ <- link file key hascontent
+		params <- ifM (Annex.getState Annex.force)
+			( return [Param "-f"]
+			, return []
+			)
+		Annex.Queue.addCommand "add" (params++[Param "--"]) [file]
+		return True
+	)
diff --git a/Command/AddUrl.hs b/Command/AddUrl.hs
--- a/Command/AddUrl.hs
+++ b/Command/AddUrl.hs
@@ -22,9 +22,10 @@
 import Types.Key
 import Types.KeySource
 import Config
+import Annex.Content.Direct
 
 def :: [Command]
-def = [notDirect $ notBareRepo $ withOptions [fileOption, pathdepthOption] $
+def = [notBareRepo $ withOptions [fileOption, pathdepthOption] $
 	command "addurl" (paramRepeating paramUrl) seek "add urls to annex"]
 
 fileOption :: Option
@@ -79,6 +80,8 @@
 		case k of
 			Nothing -> stop
 			Just (key, _) -> do
+				whenM isDirect $
+					void $ addAssociatedFile key file
 				moveAnnex key tmp
 				setUrlPresent key url
 				next $ Command.Add.cleanup file key True
@@ -90,6 +93,8 @@
 	if exists
 		then do
 			let key = Backend.URL.fromUrl url size
+			whenM isDirect $
+				void $ addAssociatedFile key file
 			setUrlPresent key url
 			next $ Command.Add.cleanup file key False
 		else do
diff --git a/Command/Copy.hs b/Command/Copy.hs
--- a/Command/Copy.hs
+++ b/Command/Copy.hs
@@ -14,9 +14,8 @@
 import Annex.Wanted
 
 def :: [Command]
-def = [notDirect $ 
-	withOptions Command.Move.options $ command "copy" paramPaths seek
-		"copy content of files to/from another repository"]
+def = [withOptions Command.Move.options $ command "copy" paramPaths seek
+	"copy content of files to/from another repository"]
 
 seek :: [CommandSeek]
 seek = [withField Command.Move.toOption Remote.byName $ \to ->
diff --git a/Command/Drop.hs b/Command/Drop.hs
--- a/Command/Drop.hs
+++ b/Command/Drop.hs
@@ -20,7 +20,7 @@
 import Annex.Wanted
 
 def :: [Command]
-def = [notDirect $ withOptions [fromOption] $ command "drop" paramPaths seek
+def = [withOptions [fromOption] $ command "drop" paramPaths seek
 	"indicate content of files not currently wanted"]
 
 fromOption :: Option
diff --git a/Command/Find.hs b/Command/Find.hs
--- a/Command/Find.hs
+++ b/Command/Find.hs
@@ -20,7 +20,7 @@
 import qualified Option
 
 def :: [Command]
-def = [notDirect $ noCommit $ withOptions [formatOption, print0Option] $
+def = [noCommit $ withOptions [formatOption, print0Option] $
 	command "find" paramPaths seek "lists available files"]
 
 formatOption :: Option
diff --git a/Command/Fix.hs b/Command/Fix.hs
--- a/Command/Fix.hs
+++ b/Command/Fix.hs
@@ -23,7 +23,7 @@
 start :: FilePath -> (Key, Backend) -> CommandStart
 start file (key, _) = do
 	link <- calcGitLink file key
-	stopUnless ((/=) link <$> liftIO (readSymbolicLink file)) $ do
+	stopUnless ((/=) (Just link) <$> liftIO (catchMaybeIO $ readSymbolicLink file)) $ do
 		showStart "fix" file
 		next $ perform file link
 
diff --git a/Command/Fsck.hs b/Command/Fsck.hs
--- a/Command/Fsck.hs
+++ b/Command/Fsck.hs
@@ -34,7 +34,7 @@
 import System.Locale
 
 def :: [Command]
-def = [notDirect $ withOptions options $ command "fsck" paramPaths seek
+def = [withOptions options $ command "fsck" paramPaths seek
 	"check for problems"]
 
 fromOption :: Option
@@ -180,12 +180,18 @@
 check :: [Annex Bool] -> Annex Bool
 check cs = all id <$> sequence cs
 
-{- Checks that the file's symlink points correctly to the content. -}
+{- Checks that the file's symlink points correctly to the content.
+ -
+ - In direct mode, there is only a symlink when the content is not present.
+ -}
 fixLink :: Key -> FilePath -> Annex Bool
 fixLink key file = do
 	want <- calcGitLink file key
-	have <- liftIO $ readSymbolicLink file
-	when (want /= have) $ do
+	have <- liftIO $ catchMaybeIO $ readSymbolicLink file
+	maybe noop (go want) have
+	return True
+  where
+	go want have = when (want /= have) $ do
 		{- Version 3.20120227 had a bug that could cause content
 		 - to be stored in the wrong hash directory. Clean up
 		 - after the bug by moving the content.
@@ -203,23 +209,27 @@
 		liftIO $ removeFile file
 		liftIO $ createSymbolicLink want file
 		Annex.Queue.addCommand "add" [Param "--force", Param "--"] [file]
-	return True
 
 {- Checks that the location log reflects the current status of the key,
  - in this repository only. -}
 verifyLocationLog :: Key -> String -> Annex Bool
 verifyLocationLog key desc = do
 	present <- inAnnex key
+	direct <- isDirect
+	u <- getUUID
 	
-	-- Since we're checking that a key's file is present, throw
-	-- in a permission fixup here too.
-	when present $ do
+	{- Since we're checking that a key's file is present, throw
+	 - in a permission fixup here too. -}
+	when (present && not direct) $ do
 		file <- inRepo $ gitAnnexLocation key
 		freezeContent file
 		freezeContentDir file
 
-	u <- getUUID
-	verifyLocationLog' key desc present u (logChange key u)
+	{- In direct mode, modified files will show up as not present,
+	 - but that is expected and not something to do anything about. -}
+	if (direct && not present)
+		then return True
+		else verifyLocationLog' key desc present u (logChange key u)
 
 verifyLocationLogRemote :: Key -> String -> Remote -> Bool -> Annex Bool
 verifyLocationLogRemote key desc remote present =
@@ -248,14 +258,20 @@
 		bad s
 
 {- The size of the data for a key is checked against the size encoded in
- - the key's metadata, if available. -}
+ - the key's metadata, if available.
+ -
+ - Not checked in direct mode, because files can be changed directly.
+  -}
 checkKeySize :: Key -> Annex Bool
-checkKeySize key = do
-	file <- inRepo $ gitAnnexLocation key
-	ifM (liftIO $ doesFileExist file)
-		( checkKeySizeOr badContent key file
-		, return True
-		)
+checkKeySize key = ifM isDirect
+	( return True
+	, do
+		file <- inRepo $ gitAnnexLocation key
+		ifM (liftIO $ doesFileExist file)
+			( checkKeySizeOr badContent key file
+			, return True
+			)
+	)
 
 checkKeySizeRemote :: Key -> Remote -> Maybe FilePath -> Annex Bool
 checkKeySizeRemote _ _ Nothing = return True
@@ -283,10 +299,16 @@
 			, msg
 			]
 
+{- Runs the backend specific check on a key's content.
+ -
+ - In direct mode, this is skipped, because files can change at any time. -}
 checkBackend :: Backend -> Key -> Annex Bool
-checkBackend backend key = do
-	file <- inRepo (gitAnnexLocation key)
-	checkBackendOr badContent backend key file
+checkBackend backend key = ifM isDirect
+	( return True
+	, do
+		file <- inRepo $ gitAnnexLocation key
+		checkBackendOr badContent backend key file
+	)
 
 checkBackendRemote :: Backend -> Key -> Remote -> Maybe FilePath -> Annex Bool
 checkBackendRemote backend key remote = maybe (return True) go
diff --git a/Command/Log.hs b/Command/Log.hs
--- a/Command/Log.hs
+++ b/Command/Log.hs
@@ -36,7 +36,7 @@
 type Outputter = Bool -> POSIXTime -> [UUID] -> Annex ()
 
 def :: [Command]
-def = [notDirect $ withOptions options $
+def = [withOptions options $
 	command "log" paramPaths seek "shows location log"]
 
 options :: [Option]
diff --git a/Command/Move.hs b/Command/Move.hs
--- a/Command/Move.hs
+++ b/Command/Move.hs
@@ -19,7 +19,7 @@
 import Logs.Transfer
 
 def :: [Command]
-def = [notDirect $ withOptions options $ command "move" paramPaths seek
+def = [withOptions options $ command "move" paramPaths seek
 	"move content of files to/from another repository"]
 
 fromOption :: Option
diff --git a/Command/PreCommit.hs b/Command/PreCommit.hs
--- a/Command/PreCommit.hs
+++ b/Command/PreCommit.hs
@@ -19,7 +19,7 @@
  - And, it needs to inject unlocked files into the annex. -}
 seek :: [CommandSeek]
 seek =
-	[ withFilesToBeCommitted $ whenAnnexed Command.Fix.start
+	[ whenNotDirect $ withFilesToBeCommitted $ whenAnnexed $ Command.Fix.start
 	, withFilesUnlockedToBeCommitted start]
 
 start :: FilePath -> CommandStart
diff --git a/Command/Whereis.hs b/Command/Whereis.hs
--- a/Command/Whereis.hs
+++ b/Command/Whereis.hs
@@ -15,7 +15,7 @@
 import Logs.Trust
 
 def :: [Command]
-def = [notDirect $ noCommit $ command "whereis" paramPaths seek
+def = [noCommit $ command "whereis" paramPaths seek
 	"lists repositories that have file content"]
 
 seek :: [CommandSeek]
diff --git a/Locations.hs b/Locations.hs
--- a/Locations.hs
+++ b/Locations.hs
@@ -93,6 +93,9 @@
  -
  - When the file is not present, returns the location where the file should
  - be stored.
+ -
+ - This does not take direct mode into account, so in direct mode it is not
+ - the actual location of the file's content.
  -}
 gitAnnexLocation :: Key -> Git.Repo -> IO FilePath
 gitAnnexLocation key r
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -135,7 +135,8 @@
 		--underlaydir=/dev/null --disable-plugin=shortcut \
 		--disable-plugin=smiley \
 		--plugin=comments --set comments_pagespec="*" \
-		--exclude='news/.*'
+		--exclude='news/.*' --exclude='design/assistant/blog/*' \
+		--exclude='bugs/*' --exclude='todo/*' --exclude='forum/*'
 
 clean:
 	rm -rf $(GIT_ANNEX_TMP_BUILD_DIR) $(bins) $(mans) test configure  *.tix .hpc $(sources) \
diff --git a/Remote/Bup.hs b/Remote/Bup.hs
--- a/Remote/Bup.hs
+++ b/Remote/Bup.hs
@@ -27,6 +27,7 @@
 import Data.ByteString.Lazy.UTF8 (fromString)
 import Data.Digest.Pure.SHA
 import Utility.UserInfo
+import Annex.Content
 
 type BupRepo = String
 
@@ -120,14 +121,12 @@
 		(os ++ [Param "-n", Param (bupRef k)] ++ src)
 
 store :: Remote -> BupRepo -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
-store r buprepo k _f _p = do
-	src <- inRepo $ gitAnnexLocation k
+store r buprepo k _f _p = sendAnnex k $ \src -> do
 	params <- bupSplitParams r buprepo k [File src]
 	liftIO $ boolSystem "bup" params
 
 storeEncrypted :: Remote -> BupRepo -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted r buprepo (cipher, enck) k _p = do
-	src <- inRepo $ gitAnnexLocation k
+storeEncrypted r buprepo (cipher, enck) k _p = sendAnnex k $ \src -> do
 	params <- bupSplitParams r buprepo enck []
 	liftIO $ catchBoolIO $
 		encrypt cipher (feedFile src) $ \h ->
diff --git a/Remote/Directory.hs b/Remote/Directory.hs
--- a/Remote/Directory.hs
+++ b/Remote/Directory.hs
@@ -111,8 +111,7 @@
 withStoredFiles = withCheckedFiles doesFileExist
 
 store :: FilePath -> ChunkSize -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
-store d chunksize k _f p = do
-	src <- inRepo $ gitAnnexLocation k
+store d chunksize k _f p = sendAnnex k $ \src ->
 	metered (Just p) k $ \meterupdate -> 
 		storeHelper d chunksize k $ \dests ->
 			case chunksize of
@@ -126,8 +125,7 @@
 						=<< L.readFile src
 
 storeEncrypted :: FilePath -> ChunkSize -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted d chunksize (cipher, enck) k p = do
-	src <- inRepo $ gitAnnexLocation k
+storeEncrypted d chunksize (cipher, enck) k p = sendAnnex k $ \src -> 
 	metered (Just p) k $ \meterupdate ->
 		storeHelper d chunksize enck $ \dests ->
 			encrypt cipher (feedFile src) $ readBytes $ \b ->
diff --git a/Remote/Glacier.hs b/Remote/Glacier.hs
--- a/Remote/Glacier.hs
+++ b/Remote/Glacier.hs
@@ -23,6 +23,7 @@
 import Creds
 import Meters
 import qualified Annex
+import Annex.Content
 
 import System.Process
 
@@ -84,17 +85,15 @@
 	| keySize k == Just 0 = do
 		warning "Cannot store empty files in Glacier."
 		return False
-	| otherwise = do
-		src <- inRepo $ gitAnnexLocation k
+	| otherwise = sendAnnex k $ \src ->
 		metered (Just m) k $ \meterupdate ->
 			storeHelper r k $ streamMeteredFile src meterupdate
 
 storeEncrypted :: Remote -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted r (cipher, enck) k m = do
-	f <- inRepo $ gitAnnexLocation k
+storeEncrypted r (cipher, enck) k m = sendAnnex k $ \src -> do
 	metered (Just m) k $ \meterupdate ->
 		storeHelper r enck $ \h ->
-			encrypt cipher (feedFile f)
+			encrypt cipher (feedFile src)
 				(readBytes $ meteredWrite meterupdate h)
 
 retrieve :: Remote -> Key -> AssociatedFile -> FilePath -> Annex Bool
diff --git a/Remote/Hook.hs b/Remote/Hook.hs
--- a/Remote/Hook.hs
+++ b/Remote/Hook.hs
@@ -103,16 +103,15 @@
 			)
 
 store :: String -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
-store h k _f _p = do
-	src <- inRepo $ gitAnnexLocation k
+store h k _f _p = sendAnnex k $ \src ->
 	runHook h "store" k (Just src) $ return True
 
 storeEncrypted :: String -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted h (cipher, enck) k _p = withTmp enck $ \tmp -> do
-	src <- inRepo $ gitAnnexLocation k
-	liftIO $ encrypt cipher (feedFile src) $
-		readBytes $ L.writeFile tmp
-	runHook h "store" enck (Just tmp) $ return True
+storeEncrypted h (cipher, enck) k _p = withTmp enck $ \tmp ->
+	sendAnnex k $ \src -> do
+		liftIO $ encrypt cipher (feedFile src) $
+			readBytes $ L.writeFile tmp
+		runHook h "store" enck (Just tmp) $ return True
 
 retrieve :: String -> Key -> AssociatedFile -> FilePath -> Annex Bool
 retrieve h k _f d = runHook h "retrieve" k (Just d) $ return True
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -101,14 +101,14 @@
 	f = keyFile k
 
 store :: RsyncOpts -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
-store o k _f p = rsyncSend o p k <=< inRepo $ gitAnnexLocation k
+store o k _f p = sendAnnex k $ rsyncSend o p k
 
 storeEncrypted :: RsyncOpts -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted o (cipher, enck) k p = withTmp enck $ \tmp -> do
-	src <- inRepo $ gitAnnexLocation k
-	liftIO $ encrypt cipher (feedFile src) $
-		readBytes $ L.writeFile tmp
-	rsyncSend o p enck tmp
+storeEncrypted o (cipher, enck) k p = withTmp enck $ \tmp ->
+	sendAnnex k $ \src -> do
+		liftIO $ encrypt cipher (feedFile src) $
+			readBytes $ L.writeFile tmp
+		rsyncSend o p enck tmp
 
 retrieve :: RsyncOpts -> Key -> AssociatedFile -> FilePath -> Annex Bool
 retrieve o k _ f = untilTrue (rsyncUrls o k) $ \u -> rsyncRemote o Nothing
diff --git a/Remote/S3.hs b/Remote/S3.hs
--- a/Remote/S3.hs
+++ b/Remote/S3.hs
@@ -112,8 +112,7 @@
 			M.delete "bucket" defaults
 
 store :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
-store r k _f p = s3Action r False $ \(conn, bucket) -> do
-	src <- inRepo $ gitAnnexLocation k
+store r k _f p = s3Action r False $ \(conn, bucket) -> sendAnnex k $ \src -> do
 	res <- storeHelper (conn, bucket) r k p src
 	s3Bool res
 
@@ -121,9 +120,8 @@
 storeEncrypted r (cipher, enck) k p = s3Action r False $ \(conn, bucket) -> 
 	-- To get file size of the encrypted content, have to use a temp file.
 	-- (An alternative would be chunking to to a constant size.)
-	withTmp enck $ \tmp -> do
-		f <- inRepo $ gitAnnexLocation k
-		liftIO $ encrypt cipher (feedFile f) $
+	withTmp enck $ \tmp -> sendAnnex k $ \src -> do
+		liftIO $ encrypt cipher (feedFile src) $
 			readBytes $ L.writeFile tmp
 		res <- storeHelper (conn, bucket) r enck p tmp
 		s3Bool res
diff --git a/Remote/WebDAV.hs b/Remote/WebDAV.hs
--- a/Remote/WebDAV.hs
+++ b/Remote/WebDAV.hs
@@ -30,6 +30,7 @@
 import Crypto
 import Creds
 import Meters
+import Annex.Content
 
 type DavUrl = String
 type DavUser = B8.ByteString
@@ -82,16 +83,14 @@
 
 store :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
 store r k _f p = metered (Just p) k $ \meterupdate ->
-	davAction r False $ \(baseurl, user, pass) -> do
-		f <- inRepo $ gitAnnexLocation k
-		liftIO $ withMeteredFile f meterupdate $
+	davAction r False $ \(baseurl, user, pass) -> sendAnnex k $ \src ->
+		liftIO $ withMeteredFile src meterupdate $
 			storeHelper r k baseurl user pass
 
 storeEncrypted :: Remote -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
 storeEncrypted r (cipher, enck) k p = metered (Just p) k $ \meterupdate ->
-	davAction r False $ \(baseurl, user, pass) -> do
-		f <- inRepo $ gitAnnexLocation k
-		liftIO $ encrypt cipher (streamMeteredFile f meterupdate) $
+	davAction r False $ \(baseurl, user, pass) -> sendAnnex k $ \src ->
+		liftIO $ encrypt cipher (streamMeteredFile src meterupdate) $
 			readBytes $ storeHelper r enck baseurl user pass
 
 storeHelper :: Remote -> Key -> DavUrl -> DavUser -> DavPass -> L.ByteString -> IO Bool
diff --git a/Seek.hs b/Seek.hs
--- a/Seek.hs
+++ b/Seek.hs
@@ -20,6 +20,7 @@
 import qualified Git.LsFiles as LsFiles
 import qualified Limit
 import qualified Option
+import Config
 
 seekHelper :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> [FilePath] -> Annex [FilePath]
 seekHelper a params = do
@@ -123,3 +124,6 @@
 
 notSymlink :: FilePath -> IO Bool
 notSymlink f = liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f
+
+whenNotDirect :: CommandSeek -> CommandSeek
+whenNotDirect a params = ifM isDirect ( return [] , a params )
diff --git a/Utility/UserInfo.hs b/Utility/UserInfo.hs
--- a/Utility/UserInfo.hs
+++ b/Utility/UserInfo.hs
@@ -7,7 +7,8 @@
 
 module Utility.UserInfo (
 	myHomeDir,
-	myUserName
+	myUserName,
+	myUserGecos,
 ) where
 
 import Control.Applicative
@@ -23,6 +24,9 @@
 {- Current user's user name. -}
 myUserName :: IO String
 myUserName = myVal ["USER", "LOGNAME"] userName
+
+myUserGecos :: IO String
+myUserGecos = myVal [] userGecos
 
 myVal :: [String] -> (UserEntry -> String) -> IO String
 myVal envvars extract = maybe (extract <$> getpwent) return =<< check envvars
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,18 @@
-git-annex (3.20130103) UNRELEASED; urgency=low
+git-annex (3.20130107) unstable; urgency=low
 
   * webapp: Add UI to stop and restart assistant.
   * committer: Fix a file handle leak.
+  * assistant: Make expensive transfer scan work fully in direct mode.
+  * More commands work in direct mode repositories: find, whereis, move, copy,
+    drop, log, fsck, add, addurl.
+  * sync: No longer automatically adds files in direct mode.
+  * assistant: Detect when system is not configured with a user name,
+    and set environment to prevent git from failing.
+  * direct: Avoid hardlinking symlinks that point to the same content
+    when the content is not present.
+  * Fix transferring files to special remotes in direct mode.
 
- -- Joey Hess <joeyh@debian.org>  Thu, 03 Jan 2013 14:58:45 -0400
+ -- Joey Hess <joeyh@debian.org>  Mon, 07 Jan 2013 01:01:41 -0400
 
 git-annex (3.20130102) unstable; urgency=low
 
diff --git a/doc/assistant/release_notes.mdwn b/doc/assistant/release_notes.mdwn
--- a/doc/assistant/release_notes.mdwn
+++ b/doc/assistant/release_notes.mdwn
@@ -1,3 +1,7 @@
+## version 3.20130107
+
+This is a bugfix release.
+
 ## version 3.20130102
 
 This release makes several significant improvements to the git-annex
diff --git a/doc/assistant/thanks.mdwn b/doc/assistant/thanks.mdwn
--- a/doc/assistant/thanks.mdwn
+++ b/doc/assistant/thanks.mdwn
@@ -47,7 +47,7 @@
 Mesar Hameed, Brandur K. Holm Petersen, Takahiro Inoue, Kai Hendry,
 Stephen Youndt, Lee Roberson, Ben Strawbridge, Andrew Greenberg, Alfred Adams
 Andrew, Aaron De Vries, Monti Knazze, Jorge Canseco, Hamish, Mark Eichin,
-Sherif Abouseda, Ben Strawbridge
+Sherif Abouseda, Ben Strawbridge, chee rabbits, Pedro Côrte-Real
 
 And special thanks to Kevin McKenzie, who also gave me a login to a Mac OSX
 machine, which has proven invaluable, and Jimmy Tang who has helped
diff --git a/doc/bugs/Disconcerting_warning_from_git-annex.mdwn b/doc/bugs/Disconcerting_warning_from_git-annex.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Disconcerting_warning_from_git-annex.mdwn
@@ -0,0 +1,6 @@
+I did a "git annex add" of a bunch of files on a storage server with low IOPS, and saw this:
+
+    git-annex: /tank/Media/Pictures/.git/annex/tmp/430_32b_SHA256E-s4464838--c1785a76ee1451f602e93c99c147e214705004e541de8256d74a3be3717d15e5.jpg.log: openBinaryFile: resource busy (file is locked)
+failed
+
+How is that even possible, when the server is doing nothing else?
diff --git a/doc/bugs/OSX_app_issues/comment_7_acd73cc5c4caa88099e2d2f19947aadf._comment b/doc/bugs/OSX_app_issues/comment_7_acd73cc5c4caa88099e2d2f19947aadf._comment
new file mode 100644
--- /dev/null
+++ b/doc/bugs/OSX_app_issues/comment_7_acd73cc5c4caa88099e2d2f19947aadf._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="4.152.108.211"
+ subject="comment 7"
+ date="2013-01-05T17:48:52Z"
+ content="""
+@jsilence, this problem does not involve lsof. There was a typo in the cabal file that prevented cabal building it with hfsevents. I'm releasing a new version to cabal with this typo fixed.
+"""]]
diff --git a/doc/design/assistant/blog/day_163__free_features.mdwn b/doc/design/assistant/blog/day_163__free_features.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_163__free_features.mdwn
@@ -0,0 +1,32 @@
+There was a typo in cabal file that broke building the assistant on OSX.
+This didn't affect the autobuilds of the app, but several users building by
+hand reported problems. I made a new minor release fixing that typo, and
+also a resouce leak bug.
+
+Got a restart UI working after all. It's a hack though. It
+opens a new tab for the new assistant instance, and as most web browsers
+don't allow javascript to close tabs, the old tab is left open. At some
+point I need to add a proper thread manager to the assistant, which the
+restart code could use to kill the watcher and committer threads, and then
+I could do a clean restart, bringing up the new daemon and redirecting the
+browser to it.
+
+Found a bug in the assistant in direct mode -- the expensive transfer scan
+didn't queue uploads needed to sync to other repos in direct mode, although
+it did queue downloads. Fixing this laid some very useful groundwork for
+making more commands support direct mode, too. Got stuck for a long time
+dealing with some very strange `git-cat-file` behavior while making this
+work. Ended up putting in a workaround.
+
+After that, I found that these commands work in direct mode, without
+needing any futher changes!
+
+* `git annex find`
+* `git annex whereis`
+* `git annex copy`
+* `git annex move`
+* `git annex drop`
+* `git annex log`
+
+Enjoy! The only commands I'd like to add to this are `fsck`, `add`,
+and `addurl`...
diff --git a/doc/design/assistant/blog/day_164__bugfixes.mdwn b/doc/design/assistant/blog/day_164__bugfixes.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_164__bugfixes.mdwn
@@ -0,0 +1,17 @@
+Several bugfixes from user feedback today.
+
+Made the assistant detect misconfigured systems where git will fail to
+commit because it cannot determine the user's name or email address, and
+dummy up enough info to get git working. It makes sense for git and
+git-annex to fail at the command line on such a misconfigured system, so
+the user can fix it, but for the assistant it makes sense to plow on and just
+work.
+
+I found a big gap in direct mode -- all the special remotes expected to find
+content in the indirect mode location when transferring to the remote. It
+was easy to fix once I noticed the problem. This is a big enough bug that
+I will be making a new release in a day or so.
+
+Also, got fsck working in direct mode. It doesn't check as many things
+as in indirect mode, because direct mode files can be modified at any time.
+Still, it's usable, and useful.
diff --git a/doc/design/assistant/blog/day_165__release_day.mdwn b/doc/design/assistant/blog/day_165__release_day.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_165__release_day.mdwn
@@ -0,0 +1,14 @@
+Got `git annex add` (and `addurl`) working in direct mode. This allowed me
+to make `git annex sync` in direct mode no longer automatically add new
+files.
+
+It's also now safe to mix direct mode annexed files with regular files in
+git, in the same repository. Might have been safe all along, but I've
+tested it, and it certianly works now. You just have to be careful to not
+use `git commit -a` to commit changes to such files, since that'll also 
+stage the entire content of direct mode files.
+
+Made a minor release for these recent changes and bugfixes. Recommended if
+you're using direct mode. Had to chase down a stupid typo I made yesterday
+that caused fsck to infinite loop if it found a corrupted file. Thank
+goodness for test suites.
diff --git a/doc/direct_mode.mdwn b/doc/direct_mode.mdwn
--- a/doc/direct_mode.mdwn
+++ b/doc/direct_mode.mdwn
@@ -34,35 +34,19 @@
 
 ## use a direct mode repository
 
-Perhaps the best way to use a direct mode repository is with the git-annex
-assistant.
-
-Otherwise, the main command that's used in direct mode repositories is
-`git annex sync`. This automatically adds new files, commits all
-changed files to git, pushes them out, pulls down any changes, etc.
-
-You can also run `git annex get` to transfer the content of files into your
-direct mode repository. Or if the direct mode repository is a remote of
-some other, regular git-annex repository, you can use commands in the other
-repository like `git annex copy` and `git annex move` to transfer the
-contents of files to the direct mode repository.
+You can use most git-annex commands as usual in a direct mode repository.
+A few commands don't work in direct mode, and will refuse to do anything.
 
-You can use `git commit --staged`. (But not `git commit -a` .. It'll commit
-whole large files into git!)
+Direct mode also works well with the git-annex assistant.
 
-You can use `git log` and other git query commands.
+You can use `git commit --staged`. But not `git commit -a` .. It'll
+commit whole large files into git!
 
 ## what doesn't work in direct mode
 
-In general git-annex commands will only work in direct mode repositories on
-files whose content is not present. That's because such files are still 
-represented as symlinks, which git-annex commands know how to operate on.
-So, `git annex get` works, but `git annex drop` and `git annex move` don't,
-and things like `git annex status` show incomplete information.
-
-It's technically possible to make all git-annex commands work in direct
-mode repositories, so this might change. Check back to this page to see
-current status about what works and what doesn't.
+`git annex status` show incomplete information. A few other commands,
+like `git annex unlock` don't make sense in direct mode and will refuse to
+run.
 
 As for git commands, you can probably use some git working tree
 manipulation commands, like `git checkout` and `git revert` in useful
@@ -72,4 +56,4 @@
 
 This is one reason it's wise to make git-annex untrust your direct mode
 repositories. Still, you can lose data using these sort of git commands, so
-use extreme caution. 
+use extreme caution.
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -269,11 +269,13 @@
 * direct
 
   Switches a repository to use direct mode, where rather than symlinks to
-  files, the files are directly present in the repository. Note that many git
-  and git-annex commands will not work in direct mode; you're mostly
-  limited to using "git annex sync" and "git annex get".
+  files, the files are directly present in the repository.
 
   As part of the switch to direct mode, any changed files will be committed.
+
+  Note that git commands that operate on the work tree are often unsafe to
+  use in direct mode repositories, and can result in data loss or other
+  bad behavior.
 
 * indirect
 
diff --git a/doc/news/version_3.20121126.mdwn b/doc/news/version_3.20121126.mdwn
deleted file mode 100644
--- a/doc/news/version_3.20121126.mdwn
+++ /dev/null
@@ -1,27 +0,0 @@
-git-annex 3.20121126 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
-   * New webdav and Amazon glacier special remotes.
-   * Display a warning when a non-existing file or directory is specified.
-   * webapp: Added configurator for Box.com.
-   * webapp: Show error messages to user when testing XMPP creds.
-   * Fix build of assistant without yesod.
-   * webapp: The list of repositiories refreshes when new repositories are
-     added, including when new repository configurations are pushed in from
-     remotes.
-   * OSX: Fix RunAtLoad value in plist file.
-   * Getting a file from chunked directory special remotes no longer buffers
-     it all in memory.
-   * S3: Added progress display for uploading and downloading.
-   * directory special remote: Made more efficient and robust.
-   * Bugfix: directory special remote could loop forever storing a key
-     when a too small chunksize was configured.
-   * Allow controlling whether login credentials for S3 and webdav are
-     committed to the repository, by setting embedcreds=yes|no when running
-     initremote.
-   * Added smallarchive repository group, that only archives files that are
-     in archive directories. Used by default for glacier when set up in the
-     webapp.
-   * assistant: Fixed handling of toplevel archive directory and
-     client repository group.
-   * assistant: Apply preferred content settings when a new symlink
-     is created, or a symlink gets renamed. Made archive directories work."""]]
diff --git a/doc/news/version_3.20130107.mdwn b/doc/news/version_3.20130107.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/news/version_3.20130107.mdwn
@@ -0,0 +1,13 @@
+git-annex 3.20130107 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+   * webapp: Add UI to stop and restart assistant.
+   * committer: Fix a file handle leak.
+   * assistant: Make expensive transfer scan work fully in direct mode.
+   * More commands work in direct mode repositories: find, whereis, move, copy,
+     drop, log, fsck, add, addurl.
+   * sync: No longer automatically adds files in direct mode.
+   * assistant: Detect when system is not configured with a user name,
+     and set environment to prevent git from failing.
+   * direct: Avoid hardlinking symlinks that point to the same content
+     when the content is not present.
+   * Fix transferring files to special remotes in direct mode."""]]
diff --git a/doc/preferred_content.mdwn b/doc/preferred_content.mdwn
--- a/doc/preferred_content.mdwn
+++ b/doc/preferred_content.mdwn
@@ -58,15 +58,6 @@
 the repo it's being dropped from. This is different than running `git annex
 drop --copies=2`, which will drop files that current have 2 copies.
 
-A wrinkle of this approach is how `in=` is handled. When deciding if
-content should be dropped, git-annex looks at the current status, not
-the status if the content would be dropped. So `in=here` means that
-any currently present content is preferred, which can be useful if you
-want manual control over content. Meanwhile `not (in=here)` should be
-avoided -- it will cause content that's not here to be preferred,
-but once the content arrives, it'll stop being preferred and will be
-dropped again!
-
 ## difference: "present"
 
 There's a special "present" keyword you can use in a preferred content
diff --git a/git-annex.1 b/git-annex.1
--- a/git-annex.1
+++ b/git-annex.1
@@ -240,11 +240,13 @@
 .IP
 .IP "direct"
 Switches a repository to use direct mode, where rather than symlinks to
-files, the files are directly present in the repository. Note that many git
-and git\-annex commands will not work in direct mode; you're mostly
-limited to using "git annex sync" and "git annex get".
+files, the files are directly present in the repository.
 .IP
 As part of the switch to direct mode, any changed files will be committed.
+.IP
+Note that git commands that operate on the work tree are often unsafe to
+use in direct mode repositories, and can result in data loss or other
+bad behavior.
 .IP
 .IP "indirect"
 Switches a repository back from direct mode to the default, indirect mode.
diff --git a/git-annex.cabal b/git-annex.cabal
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 3.20130105
+Version: 3.20130107
 Cabal-Version: >= 1.8
 License: GPL
 Maintainer: Joey Hess <joey@kitenet.net>
@@ -99,7 +99,7 @@
   if flag(Webapp) && flag(Assistant)
     Build-Depends: yesod, yesod-static, case-insensitive,
      http-types, transformers, wai, wai-logger, warp, blaze-builder,
-     crypto-api, hamlet, clientsession, aeson, yesod-form,
+     crypto-api, hamlet, clientsession, aeson, yesod-form (>= 1.2.0),
      template-haskell, yesod-default (>= 1.1.0), data-default
     CPP-Options: -DWITH_WEBAPP
 
