diff --git a/Makefile b/Makefile
index b7f0e79db2f65757e2d2c678ac12103628460dd1..dd259e5cdf7c99a74d73489f90ccbf7d33d86ee9 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,7 @@ makefiles = \
   src/resolve-system-dependencies/local.mk \
   scripts/local.mk \
   misc/bash/local.mk \
+  misc/fish/local.mk \
   misc/zsh/local.mk \
   misc/systemd/local.mk \
   misc/launchd/local.mk \
diff --git a/doc/manual/generate-builtins.nix b/doc/manual/generate-builtins.nix
index 416a7fdba1c985c07f442a58b833a49c4f509939..92c7b1a318738b565891f86fb6e6f6240972bbe4 100644
--- a/doc/manual/generate-builtins.nix
+++ b/doc/manual/generate-builtins.nix
@@ -6,9 +6,11 @@ builtins:
 concatStrings (map
   (name:
     let builtin = builtins.${name}; in
-    "  - `builtins.${name}` " + concatStringsSep " " (map (s: "*${s}*") builtin.args)
-    + "  \n\n"
-    + concatStrings (map (s: "    ${s}\n") (splitLines builtin.doc)) + "\n\n"
+    "<dt><code>${name} "
+    + concatStringsSep " " (map (s: "<var>${s}</var>") builtin.args)
+    + "</code></dt>"
+    + "<dd>\n\n"
+    + builtin.doc
+    + "\n\n</dd>"
   )
   (attrNames builtins))
-
diff --git a/doc/manual/local.mk b/doc/manual/local.mk
index 271529b3804d9e11cbaaeed9254ce2a586747277..e25157af80bcfdb23dd25cfda221545ca8de34f0 100644
--- a/doc/manual/local.mk
+++ b/doc/manual/local.mk
@@ -64,6 +64,7 @@ $(d)/conf-file.json: $(bindir)/nix
 $(d)/src/expressions/builtins.md: $(d)/builtins.json $(d)/generate-builtins.nix $(d)/src/expressions/builtins-prefix.md $(bindir)/nix
 	@cat doc/manual/src/expressions/builtins-prefix.md > $@.tmp
 	$(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtins.nix (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp
+	@cat doc/manual/src/expressions/builtins-suffix.md >> $@.tmp
 	@mv $@.tmp $@
 
 $(d)/builtins.json: $(bindir)/nix
diff --git a/doc/manual/src/expressions/builtins-prefix.md b/doc/manual/src/expressions/builtins-prefix.md
index c16b2805fd250a7299d21fa030218c01a77dfcf0..87127de2acda421549f14b92b554c0f86c67dd09 100644
--- a/doc/manual/src/expressions/builtins-prefix.md
+++ b/doc/manual/src/expressions/builtins-prefix.md
@@ -9,7 +9,8 @@ scope. Instead, you can access them through the `builtins` built-in
 value, which is a set that contains all built-in functions and values.
 For instance, `derivation` is also available as `builtins.derivation`.
 
-  - `derivation` *attrs*; `builtins.derivation` *attrs*\
-
-    `derivation` is described in [its own section](derivations.md).
-
+<dl>
+  <dt><code>derivation <var>attrs</var></code>;
+      <code>builtins.derivation <var>attrs</var></code></dt>
+  <dd><p><var>derivation</var> in described in
+         <a href="derivations.md">its own section</a>.</p></dd>
diff --git a/doc/manual/src/expressions/builtins-suffix.md b/doc/manual/src/expressions/builtins-suffix.md
new file mode 100644
index 0000000000000000000000000000000000000000..a74db28579e3f6e89b9fea6f5bb1aac7759e8bbe
--- /dev/null
+++ b/doc/manual/src/expressions/builtins-suffix.md
@@ -0,0 +1 @@
+</dl>
diff --git a/misc/fish/completion.fish b/misc/fish/completion.fish
new file mode 100644
index 0000000000000000000000000000000000000000..bedbefaf8c90c4532ec09dea94707ac12a79f094
--- /dev/null
+++ b/misc/fish/completion.fish
@@ -0,0 +1,37 @@
+function _nix_complete
+  # Get the current command up to a cursor.
+  # - Behaves correctly even with pipes and nested in commands like env.
+  # - TODO: Returns the command verbatim (does not interpolate variables).
+  #   That might not be optimal for arguments like -f.
+  set -l nix_args (commandline --current-process --tokenize --cut-at-cursor)
+  # --cut-at-cursor with --tokenize removes the current token so we need to add it separately.
+  # https://github.com/fish-shell/fish-shell/issues/7375
+  # Can be an empty string.
+  set -l current_token (commandline --current-token --cut-at-cursor)
+
+  # Nix wants the index of the argv item to complete but the $nix_args variable
+  # also contains the program name (argv[0]) so we would need to subtract 1.
+  # But the variable also misses the current token so it cancels out.
+  set -l nix_arg_to_complete (count $nix_args)
+
+  env NIX_GET_COMPLETIONS=$nix_arg_to_complete $nix_args $current_token
+end
+
+function _nix_accepts_files
+  set -l response (_nix_complete)
+  # First line is either filenames or no-filenames.
+  test $response[1] = 'filenames'
+end
+
+function _nix
+  set -l response (_nix_complete)
+  # Skip the first line since it handled by _nix_accepts_files.
+  # Tail lines each contain a command followed by a tab character and, optionally, a description.
+  # This is also the format fish expects.
+  string collect -- $response[2..-1]
+end
+
+# Disable file path completion if paths do not belong in the current context.
+complete --command nix --condition 'not _nix_accepts_files' --no-files
+
+complete --command nix --arguments '(_nix)'
diff --git a/misc/fish/local.mk b/misc/fish/local.mk
new file mode 100644
index 0000000000000000000000000000000000000000..ece899fc3cb2c50e26e10c23b0289f483ae41709
--- /dev/null
+++ b/misc/fish/local.mk
@@ -0,0 +1 @@
+$(eval $(call install-file-as, $(d)/completion.fish, $(datarootdir)/fish/vendor_completions.d/nix.fish, 0644))
diff --git a/scripts/install.in b/scripts/install.in
index e801d4268c398604191db6d4bd29cf2d0a0add9e..ffc1f2785b89f824a1516272f0c0159f9cb19343 100755
--- a/scripts/install.in
+++ b/scripts/install.in
@@ -56,7 +56,7 @@ case "$(uname -s).$(uname -m)" in
         system=x86_64-darwin
         ;;
     Darwin.arm64|Darwin.aarch64)
-        hash=@binaryTarball_aarch64-darwin@
+        hash=@tarballHash_aarch64-darwin@
         path=@tarballPath_aarch64-darwin@
         system=aarch64-darwin
         ;;
diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc
index fe52912cf95ad30badd11724daeb292e272b333d..5f263061b92c6bb86d4f16700233f56fbed4b8b3 100644
--- a/src/libcmd/installables.cc
+++ b/src/libcmd/installables.cc
@@ -171,14 +171,50 @@ Strings SourceExprCommand::getDefaultFlakeAttrPathPrefixes()
 
 void SourceExprCommand::completeInstallable(std::string_view prefix)
 {
-    if (file) return; // FIXME
-
-    completeFlakeRefWithFragment(
-        getEvalState(),
-        lockFlags,
-        getDefaultFlakeAttrPathPrefixes(),
-        getDefaultFlakeAttrPaths(),
-        prefix);
+    if (file) {
+        evalSettings.pureEval = false;
+        auto state = getEvalState();
+        Expr *e = state->parseExprFromFile(
+            resolveExprPath(state->checkSourcePath(lookupFileArg(*state, *file)))
+        );
+
+        Value root;
+        state->eval(e, root);
+
+        auto autoArgs = getAutoArgs(*state);
+
+        std::string prefix_ = std::string(prefix);
+        auto sep = prefix_.rfind('.');
+        std::string searchWord;
+        if (sep != std::string::npos) {
+            searchWord = prefix_.substr(sep, std::string::npos);
+            prefix_ = prefix_.substr(0, sep);
+        } else {
+            searchWord = prefix_;
+            prefix_ = "";
+        }
+
+        Value &v1(*findAlongAttrPath(*state, prefix_, *autoArgs, root).first);
+        state->forceValue(v1);
+        Value v2;
+        state->autoCallFunction(*autoArgs, v1, v2);
+
+        if (v2.type() == nAttrs) {
+            for (auto & i : *v2.attrs) {
+                std::string name = i.name;
+                if (name.find(searchWord) == 0) {
+                    completions->add(i.name);
+                }
+            }
+        }
+    } else {
+        completeFlakeRefWithFragment(
+            getEvalState(),
+            lockFlags,
+            getDefaultFlakeAttrPathPrefixes(),
+            getDefaultFlakeAttrPaths(),
+            prefix);
+    }
 }
 
 void completeFlakeRefWithFragment(
diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc
index b8b99d4fa8dcd800341618d7dfcb6fa69091bc76..c593400a72a21448fcc5c530ac79a17f57b9c7f3 100644
--- a/src/libexpr/primops/fetchTree.cc
+++ b/src/libexpr/primops/fetchTree.cc
@@ -7,6 +7,7 @@
 
 #include <ctime>
 #include <iomanip>
+#include <regex>
 
 namespace nix {
 
@@ -60,10 +61,19 @@ void emitTreeAttrs(
     v.attrs->sort();
 }
 
-std::string fixURI(std::string uri, EvalState &state)
+std::string fixURI(std::string uri, EvalState &state, const std::string & defaultScheme = "file")
 {
     state.checkURI(uri);
-    return uri.find("://") != std::string::npos ? uri : "file://" + uri;
+    return uri.find("://") != std::string::npos ? uri : defaultScheme + "://" + uri;
+}
+
+std::string fixURIForGit(std::string uri, EvalState & state)
+{
+    static std::regex scp_uri("([^/].*)@(.*):(.*)");
+    if (uri[0] != '/' && std::regex_match(uri, scp_uri))
+        return fixURI(std::regex_replace(uri, scp_uri, "$1@$2/$3"), state, "ssh");
+    else
+        return fixURI(uri, state);
 }
 
 void addURI(EvalState &state, fetchers::Attrs &attrs, Symbol name, std::string v)
@@ -121,15 +131,15 @@ static void fetchTree(
 
         input = fetchers::Input::fromAttrs(std::move(attrs));
     } else {
-        auto url = fixURI(state.coerceToString(pos, *args[0], context, false, false), state);
+        auto url = state.coerceToString(pos, *args[0], context, false, false);
 
         if (type == "git") {
             fetchers::Attrs attrs;
             attrs.emplace("type", "git");
-            attrs.emplace("url", url);
+            attrs.emplace("url", fixURIForGit(url, state));
             input = fetchers::Input::fromAttrs(std::move(attrs));
         } else {
-            input = fetchers::Input::fromURL(url);
+            input = fetchers::Input::fromURL(fixURI(url, state));
         }
     }
 
diff --git a/src/libstore/machines.cc b/src/libstore/machines.cc
index b42e5e4344e119e77ef14f9ea5860c42d21ea147..9843ccf045e751be1eded5a2a0da5b989123feb6 100644
--- a/src/libstore/machines.cc
+++ b/src/libstore/machines.cc
@@ -16,13 +16,18 @@ Machine::Machine(decltype(storeUri) storeUri,
     decltype(mandatoryFeatures) mandatoryFeatures,
     decltype(sshPublicHostKey) sshPublicHostKey) :
     storeUri(
-        // Backwards compatibility: if the URI is a hostname,
-        // prepend ssh://.
+        // Backwards compatibility: if the URI is schemeless, is not a path,
+        // and is not one of the special store connection words, prepend
+        // ssh://.
         storeUri.find("://") != std::string::npos
-        || hasPrefix(storeUri, "local")
-        || hasPrefix(storeUri, "remote")
-        || hasPrefix(storeUri, "auto")
-        || hasPrefix(storeUri, "/")
+        || storeUri.find("/") != std::string::npos
+        || storeUri == "auto"
+        || storeUri == "daemon"
+        || storeUri == "local"
+        || hasPrefix(storeUri, "auto?")
+        || hasPrefix(storeUri, "daemon?")
+        || hasPrefix(storeUri, "local?")
+        || hasPrefix(storeUri, "?")
         ? storeUri
         : "ssh://" + storeUri),
     systemTypes(systemTypes),