diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 25b50da7e..ec751ad31 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -179,6 +179,18 @@ string showType(const Value & v)
 }
 
 
+bool Value::isTrivial() const
+{
+    return
+        type != tApp
+        && type != tPrimOpApp
+        && (type != tThunk
+            || (dynamic_cast<ExprAttrs *>(thunk.expr)
+                && ((ExprAttrs *) thunk.expr)->dynamicAttrs.empty())
+            || dynamic_cast<ExprLambda *>(thunk.expr));
+}
+
+
 #if HAVE_BOEHMGC
 /* Called when the Boehm GC runs out of memory. */
 static void * oomHandler(size_t requested)
@@ -749,7 +761,7 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
 }
 
 
-void EvalState::evalFile(const Path & path_, Value & v)
+void EvalState::evalFile(const Path & path_, Value & v, bool mustBeTrivial)
 {
     auto path = checkSourcePath(path_);
 
@@ -778,6 +790,11 @@ void EvalState::evalFile(const Path & path_, Value & v)
     fileParseCache[path2] = e;
 
     try {
+        // Enforce that 'flake.nix' is a direct attrset, not a
+        // computation.
+        if (mustBeTrivial &&
+            !(dynamic_cast<ExprAttrs *>(e)))
+            throw Error("file '%s' must be an attribute set", path);
         eval(e, v);
     } catch (Error & e) {
         addErrorPrefix(e, "while evaluating the file '%1%':\n", path2);
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index 468a826ca..16350a5bf 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -157,8 +157,9 @@ public:
     Expr * parseStdin();
 
     /* Evaluate an expression read from the given file to normal
-       form. */
-    void evalFile(const Path & path, Value & v);
+       form. Optionally enforce that the top-level expression is
+       trivial (i.e. doesn't require arbitrary computation). */
+    void evalFile(const Path & path, Value & v, bool mustBeTrivial = false);
 
     void resetFileCache();
 
diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc
index c10906731..050e65259 100644
--- a/src/libexpr/flake/flake.cc
+++ b/src/libexpr/flake/flake.cc
@@ -198,9 +198,7 @@ static SourceInfo fetchFlake(EvalState & state, const FlakeRef & resolvedRef)
 static void expectType(EvalState & state, ValueType type,
     Value & value, const Pos & pos)
 {
-    if (value.type == tThunk &&
-        ((type == tAttrs && dynamic_cast<ExprAttrs *>(value.thunk.expr)) ||
-         (type == tLambda && dynamic_cast<ExprLambda *>(value.thunk.expr))))
+    if (value.type == tThunk && value.isTrivial())
         state.forceValue(value, pos);
     if (value.type != type)
         throw Error("expected %s but got %s at %s",
@@ -231,8 +229,7 @@ Flake getFlake(EvalState & state, const FlakeRef & flakeRef)
         throw Error("source tree referenced by '%s' does not contain a '%s/flake.nix' file", resolvedRef, resolvedRef.subdir);
 
     Value vInfo;
-    // FIXME: don't evaluate vInfo.
-    state.evalFile(realFlakeFile, vInfo); // FIXME: symlink attack
+    state.evalFile(realFlakeFile, vInfo, true); // FIXME: symlink attack
 
     expectType(state, tAttrs, vInfo, Pos(state.symbols.create(realFlakeFile), 0, 0));
 
diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh
index e1ec87d3b..bdf2cdde1 100644
--- a/src/libexpr/value.hh
+++ b/src/libexpr/value.hh
@@ -170,6 +170,11 @@ struct Value
     {
         return type == tList1 ? 1 : type == tList2 ? 2 : bigList.size;
     }
+
+    /* Check whether forcing this value requires a trivial amount of
+       computation. In particular, function applications are
+       non-trivial. */
+    bool isTrivial() const;
 };