diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2020 Torsten Schmits
+Copyright (c) 2023 Torsten Schmits
 
 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 following conditions are met:
diff --git a/hix.cabal b/hix.cabal
--- a/hix.cabal
+++ b/hix.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           hix
-version:        0.2.0.0
+version:        0.4.0.0
 synopsis:       Haskell/Nix development build tools
 description:    See https://hackage.haskell.org/package/hix/docs/Hix.html
 category:       Build
@@ -25,17 +25,26 @@
 library
   exposed-modules:
       Hix
+      Hix.Bootstrap
       Hix.Cabal
+      Hix.Compat
       Hix.Component
+      Hix.Data.BootstrapProjectConfig
+      Hix.Data.ComponentConfig
       Hix.Data.Error
       Hix.Data.GhciConfig
       Hix.Data.GhciTest
+      Hix.Data.NewProjectConfig
+      Hix.Data.PreprocConfig
+      Hix.Data.ProjectFile
       Hix.Env
       Hix.Ghci
       Hix.Json
       Hix.Monad
+      Hix.New
       Hix.Options
       Hix.Optparse
+      Hix.Prelude
       Hix.Preproc
   hs-source-dirs:
       lib
@@ -79,6 +88,7 @@
       Cabal
     , aeson >=2.0 && <2.2
     , base ==4.*
+    , casing >=0.1.4 && <0.2
     , exon ==1.4.*
     , extra ==1.7.*
     , filepattern ==0.1.*
@@ -152,9 +162,11 @@
   type: exitcode-stdio-1.0
   main-is: Main.hs
   other-modules:
+      Hix.Test.BootstrapTest
       Hix.Test.CabalFile
       Hix.Test.CabalTest
       Hix.Test.GhciTest
+      Hix.Test.NewTest
       Hix.Test.PreprocTest
   hs-source-dirs:
       test
diff --git a/lib/Hix.hs b/lib/Hix.hs
--- a/lib/Hix.hs
+++ b/lib/Hix.hs
@@ -2,13 +2,23 @@
 
 import Path.IO (getCurrentDir)
 
-import Hix.Data.Error (Error (..), printEnvError, printFatalError, printGhciError, printPreprocError)
+import Hix.Bootstrap (bootstrapProject)
+import Hix.Data.Error (
+  Error (..),
+  printBootstrapError,
+  printEnvError,
+  printFatalError,
+  printGhciError,
+  printNewError,
+  printPreprocError,
+  )
 import Hix.Env (printEnvRunner)
 import Hix.Ghci (printGhciCmdline, printGhcidCmdline)
 import Hix.Monad (M, runM)
+import Hix.New (newProject)
 import qualified Hix.Options as Options
 import Hix.Options (
-  Command (EnvRunner, GhciCmd, GhcidCmd, Preproc),
+  Command (BootstrapCmd, EnvRunner, GhciCmd, GhcidCmd, NewCmd, Preproc),
   GlobalOptions (GlobalOptions),
   Options (Options),
   parseCli,
@@ -24,6 +34,8 @@
   PreprocError err -> printPreprocError err
   EnvError err -> printEnvError err
   GhciError err -> printGhciError err
+  NewError err -> printNewError err
+  BootstrapError err -> printBootstrapError err
   NoMatch msg | fromMaybe False verbose -> printPreprocError msg
   NoMatch _ -> unit
   Fatal err -> printFatalError err
@@ -34,6 +46,8 @@
   EnvRunner opts -> printEnvRunner opts.options
   GhcidCmd opts -> printGhcidCmdline opts
   GhciCmd opts -> printGhciCmdline opts
+  NewCmd opts -> newProject opts.config
+  BootstrapCmd opts -> bootstrapProject opts.config
 
 main :: IO ()
 main = do
diff --git a/lib/Hix/Bootstrap.hs b/lib/Hix/Bootstrap.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Bootstrap.hs
@@ -0,0 +1,401 @@
+module Hix.Bootstrap where
+
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Reader (ask)
+import Data.List.NonEmpty ((<|))
+import qualified Data.Text as Text
+import Distribution.Compiler (PerCompilerFlavor (PerCompilerFlavor))
+import qualified Distribution.PackageDescription as Cabal
+import Distribution.PackageDescription (
+  BuildInfo,
+  GenericPackageDescription,
+  PackageDescription,
+  UnqualComponentName,
+  buildType,
+  licenseFiles,
+  unPackageName,
+  unUnqualComponentName,
+  )
+import Distribution.Pretty (Pretty, pretty)
+import Distribution.Simple (Dependency (Dependency), depVerRange)
+import Distribution.Types.PackageDescription (license)
+import Distribution.Utils.ShortText (ShortText, fromShortText)
+import qualified Distribution.Verbosity as Cabal
+import Exon (exon)
+import Path (Abs, Dir, File, Path, Rel, parent, parseRelFile, relfile, toFilePath, (</>))
+import System.FilePattern.Directory (getDirectoryFilesIgnore)
+
+import Hix.Compat (readGenericPackageDescription)
+import qualified Hix.Data.BootstrapProjectConfig
+import Hix.Data.BootstrapProjectConfig (BootstrapProjectConfig)
+import qualified Hix.Data.ComponentConfig
+import Hix.Data.ComponentConfig (PackageName (PackageName))
+import Hix.Data.Error (pathText, tryIO)
+import qualified Hix.Data.NewProjectConfig
+import qualified Hix.Data.ProjectFile
+import Hix.Data.ProjectFile (ProjectFile (ProjectFile), createFile)
+import qualified Hix.Monad
+import Hix.Monad (Env (Env), M, noteBootstrap)
+import qualified Hix.Prelude
+import Hix.Prelude (Prelude, findPrelude)
+
+data ExprAttr =
+  ExprAttr {
+    name :: Text,
+    value :: Expr
+  }
+  |
+  ExprAttrNil
+  deriving stock (Eq, Show, Generic)
+
+data Expr =
+  ExprString Text
+  |
+  ExprLit Text
+  |
+  ExprList [Expr]
+  |
+  ExprAttrs [ExprAttr]
+  |
+  ExprPrefix Text Expr
+  deriving stock (Eq, Show, Generic)
+
+exprStrings :: [Text] -> Expr
+exprStrings =
+  ExprList . fmap ExprString
+
+data CabalInfo =
+  CabalInfo {
+    path :: Path Rel Dir,
+    info :: GenericPackageDescription
+  }
+  deriving stock (Eq, Show, Generic)
+
+data ComponentType =
+  Library
+  |
+  Executable Text
+  |
+  Benchmark Text
+  |
+  Test Text
+  deriving stock (Eq, Show, Generic)
+
+data PreludeWithVersion =
+  PreludeWithVersion {
+    prelude :: Prelude,
+    dep :: Maybe Dependency
+  }
+  deriving stock (Eq, Show, Generic)
+
+data HixComponent =
+  HixComponent {
+    special :: ComponentType,
+    known :: [ExprAttr],
+    prelude :: Maybe PreludeWithVersion
+  }
+  deriving stock (Eq, Show, Generic)
+
+data HixPackage =
+  HixPackage {
+    name :: PackageName,
+    src :: Path Rel Dir,
+    known :: [ExprAttr],
+    meta :: [ExprAttr],
+    description :: ExprAttr,
+    components :: [HixComponent]
+  }
+  deriving stock (Eq, Show, Generic)
+
+indent ::
+  Functor t =>
+  Int ->
+  t Text ->
+  t Text
+indent n =
+  fmap (Text.replicate n " " <>)
+
+withSemicolon :: NonEmpty Text -> NonEmpty Text
+withSemicolon = \case
+  e :| [] ->
+    [e <> ";"]
+  h :| h1 : t -> h <| withSemicolon (h1 :| t)
+
+renderAttrs :: Int -> [ExprAttr] -> [Text]
+renderAttrs ind attrs =
+  attrs >>= \case
+    ExprAttr k v ->
+      case renderExpr ind v of
+        e :| [] -> [[exon|#{k} = #{e};|]]
+        h :| (h1 : t) -> [exon|#{k} = #{h}|] : toList (withSemicolon (h1 :| t))
+    ExprAttrNil ->
+      []
+
+renderExpr :: Int -> Expr -> NonEmpty Text
+renderExpr ind = \case
+  ExprString s -> indent ind [[exon|"#{s}"|]]
+  ExprLit e -> [e]
+  ExprList l -> "[" :| (indent (ind + 2) (toList . renderExpr ind =<< l)) ++ ["]"]
+  ExprAttrs a -> case renderAttrs ind a of
+    [] -> ["{};"]
+    as -> "{" :| indent (ind + 2) as ++ ["}"]
+  ExprPrefix p (renderExpr ind -> h :| t) ->
+    [exon|#{p} #{h}|] :| t
+
+renderRootExpr :: Expr -> Text
+renderRootExpr =
+  Text.unlines . toList . renderExpr 0
+
+readCabal ::
+  Path Abs Dir ->
+  Path Rel File ->
+  M CabalInfo
+readCabal root path = do
+  info <- liftIO (readGenericPackageDescription Cabal.verbose (toFilePath (root </> path)))
+  pure CabalInfo {path = dir, info}
+  where
+    dir = parent path
+
+class RenderCabalOption a where
+  renderCabalOption :: a -> Text
+
+instance {-# overlappable #-} Pretty a => RenderCabalOption a where
+  renderCabalOption = show . pretty
+
+instance RenderCabalOption ShortText where
+  renderCabalOption = toText . fromShortText
+
+instance RenderCabalOption String where
+  renderCabalOption = toText
+
+checkEmpty ::
+  Text ->
+  Expr ->
+  ExprAttr
+checkEmpty key = \case
+  ExprString value | Text.null value ->
+    ExprAttrNil
+  ExprList value | null value ->
+    ExprAttrNil
+  value ->
+    ExprAttr key value
+
+singleOpt ::
+  RenderCabalOption a =>
+  Text ->
+  (e -> Maybe a) ->
+  e ->
+  ExprAttr
+singleOpt key get entity =
+  maybe ExprAttrNil (checkEmpty key . ExprString) (renderCabalOption <$> get entity)
+
+single ::
+  RenderCabalOption a =>
+  Text ->
+  (e -> a) ->
+  e ->
+  ExprAttr
+single key get =
+  singleOpt key (Just . get)
+
+multiOpt ::
+  RenderCabalOption a =>
+  Text ->
+  (e -> Maybe [a]) ->
+  e ->
+  ExprAttr
+multiOpt key get entity =
+  maybe ExprAttrNil (checkEmpty key . exprStrings) (fmap renderCabalOption <$> get entity)
+
+multi ::
+  RenderCabalOption a =>
+  Text ->
+  (e -> [a]) ->
+  e ->
+  ExprAttr
+multi key get =
+  multiOpt key (Just . get)
+
+multiOrSingle ::
+  ∀ a e .
+  RenderCabalOption a =>
+  Text ->
+  (e -> [a]) ->
+  e ->
+  ExprAttr
+multiOrSingle key get entity =
+  check (renderCabalOption <$> get entity)
+  where
+    check :: [Text] -> ExprAttr
+    check [] = ExprAttrNil
+    check [sing] = ExprAttr key (ExprString sing)
+    check values = ExprAttr key (exprStrings values)
+
+mkAttrs :: [e -> ExprAttr] -> e -> [ExprAttr]
+mkAttrs a e =
+  (fmap ($ e) a)
+
+-- TODO extract version and put it in a file
+knownPackageKeys :: PackageDescription -> [ExprAttr]
+knownPackageKeys =
+  mkAttrs [
+    single "author" (.author),
+    single "build-type" buildType,
+    single "copyright" (.copyright),
+    single "license" license,
+    singleOpt "license-file" (head . licenseFiles)
+  ]
+
+metaPackageKeys :: PackageDescription -> [ExprAttr]
+metaPackageKeys =
+  mkAttrs [
+    single "maintainer" (.maintainer),
+    single "homepage" (.homepage),
+    single "synopsis" (.synopsis)
+  ]
+
+ghcFlavour :: PerCompilerFlavor a -> a
+ghcFlavour (PerCompilerFlavor a _) = a
+
+notDefaultGhcOption :: String -> Bool
+notDefaultGhcOption = \case
+  "-threaded" -> False
+  "-rtsopts" -> False
+  "-with-rtsopts=-N" -> False
+  _ -> True
+
+knownComponentKeys :: Maybe Prelude -> BuildInfo -> (Maybe PreludeWithVersion, [ExprAttr])
+knownComponentKeys prelude info =
+  (preludeWithVersion, vals)
+  where
+    vals =
+      mkAttrs [
+        multi "dependencies" (const deps),
+        multi "default-extensions" (.defaultExtensions),
+        multiOrSingle "source-dirs" (.hsSourceDirs),
+        singleOpt "language" (.defaultLanguage),
+        multi "ghc-options" (filter notDefaultGhcOption . ghcFlavour . (.options))
+      ] info
+    (preludeWithVersion, deps)
+      | Just p <- prelude =
+        let (v, res) = foldl (amendPrelude p) (Nothing, []) info.targetBuildDepends
+        in (Just (PreludeWithVersion p v), res)
+      | otherwise =
+        (Nothing, filter notBase (info.targetBuildDepends))
+    amendPrelude p (Nothing, ds) dep@(Dependency (Cabal.unPackageName -> dname) _ _) | dname == p.preludePackage =
+      (Just dep, ds)
+    amendPrelude _ (v, ds) d = (v, d : ds)
+
+notBase :: Cabal.Dependency -> Bool
+notBase = \case
+  Cabal.Dependency "base" _ _ -> False
+  _ -> True
+
+convertComponent :: ComponentType -> BuildInfo -> HixComponent
+convertComponent special info =
+  HixComponent {..}
+  where
+    (prelude, known) = knownComponentKeys preludeBasic info
+    preludeBasic = findPrelude info.mixins
+
+convertLibrary :: Cabal.Library -> HixComponent
+convertLibrary lib =
+  convertComponent Library lib.libBuildInfo
+
+convertExecutable :: UnqualComponentName -> Cabal.Executable -> HixComponent
+convertExecutable name exe =
+  convertComponent (Executable (toText (unUnqualComponentName name))) exe.buildInfo
+
+convertTestsuite :: UnqualComponentName -> Cabal.TestSuite -> HixComponent
+convertTestsuite name test =
+  convertComponent (Test (toText (unUnqualComponentName name))) test.testBuildInfo
+
+convertBenchmark :: UnqualComponentName -> Cabal.Benchmark -> HixComponent
+convertBenchmark name bench =
+  convertComponent (Benchmark (toText (unUnqualComponentName name))) bench.benchmarkBuildInfo
+
+convert :: CabalInfo -> HixPackage
+convert cinfo =
+  HixPackage {
+    name = PackageName (toText (unPackageName pkg.package.pkgName)),
+    src = cinfo.path,
+    known = knownPackageKeys pkg,
+    meta = metaPackageKeys pkg,
+    description = single "description" (.description) pkg,
+    components
+  }
+  where
+    components =
+      maybeToList (convertLibrary . (.condTreeData) <$> info.condLibrary)
+      <>
+      (uncurry convertExecutable . second (.condTreeData) <$> info.condExecutables)
+      <>
+      (uncurry convertTestsuite . second (.condTreeData) <$> info.condTestSuites)
+      <>
+      (uncurry convertBenchmark . second (.condTreeData) <$> info.condBenchmarks)
+    pkg = info.packageDescription
+    info = cinfo.info
+
+renderComponent :: HixComponent -> ExprAttr
+renderComponent HixComponent {..} =
+  ExprAttr key cabalConfig
+  where
+    cabalConfig = ExprAttrs (known <> foldMap preludeAttrs prelude)
+    preludeAttrs p =
+      [ExprAttr "prelude" (ExprAttrs [
+        ExprAttr "package" (preludePackageAttrs p),
+        ExprAttr "module" (ExprString (toText p.prelude.preludeModule))
+      ])]
+    preludePackageAttrs p
+      | Just dep <- p.dep =
+        ExprAttrs [
+          (ExprAttr "name" (ExprString (toText p.prelude.preludePackage))),
+          (ExprAttr "version" (ExprString (show (pretty (depVerRange dep)))))
+        ]
+      | otherwise = ExprString (toText p.prelude.preludePackage)
+    key = case special of
+      Library -> "library"
+      Executable name -> [exon|executables.#{name}|]
+      Test name -> [exon|tests.#{name}|]
+      Benchmark name -> [exon|benchmarks.#{name}|]
+
+flakePackage :: HixPackage -> ExprAttr
+flakePackage pkg =
+  ExprAttr name attrs
+  where
+    attrs = ExprAttrs (src : pkg.description : (ExprAttr "cabal" cabalConfig : comps))
+    name = pkg.name.unPackageName
+    src = ExprAttr "src" (ExprLit [exon|./#{Text.dropWhileEnd ('/' ==) (pathText pkg.src)}|])
+    cabalConfig = ExprAttrs (pkg.known <> (if null pkg.meta then [] else [ExprAttr "meta" (ExprAttrs pkg.meta)]))
+    comps = renderComponent <$> pkg.components
+
+mainPackage :: [HixPackage] -> ExprAttr
+mainPackage = \case
+  pkg : _ : _ -> ExprAttr "main" (ExprString pkg.name.unPackageName)
+  _ -> ExprAttrNil
+
+flake :: BootstrapProjectConfig -> [HixPackage] -> Expr
+flake conf pkgs =
+  ExprAttrs [
+    (ExprAttr "description" (ExprString "A Haskell project")),
+    (ExprAttr "inputs.hix.url" (ExprString conf.hixUrl.unHixUrl)),
+    (ExprAttr "outputs" (ExprPrefix "{hix, ...}: hix.lib.flake" (ExprAttrs [
+      (ExprAttr "packages" (ExprAttrs (flakePackage <$> pkgs))),
+      mainPackage pkgs
+    ])))
+  ]
+
+bootstrapFiles :: BootstrapProjectConfig -> M [ProjectFile]
+bootstrapFiles conf = do
+  Env {root} <- ask
+  cabals <- paths =<< lift (tryIO (getDirectoryFilesIgnore (toFilePath root) ["**/*.cabal"] ["dist-newstyle/**"]))
+  pkgs <- fmap convert <$> traverse (readCabal root) cabals
+  pure [
+    ProjectFile {path = [relfile|flake.nix|], content = renderRootExpr (flake conf pkgs)}
+    ]
+  where
+    paths = traverse (noteBootstrap "File path error" . parseRelFile)
+
+bootstrapProject :: BootstrapProjectConfig -> M ()
+bootstrapProject conf =
+  traverse_ createFile =<< bootstrapFiles conf
diff --git a/lib/Hix/Cabal.hs b/lib/Hix/Cabal.hs
--- a/lib/Hix/Cabal.hs
+++ b/lib/Hix/Cabal.hs
@@ -1,14 +1,7 @@
-{-# language CPP #-}
-
 module Hix.Cabal where
 
 import Control.Monad.Trans.Except (ExceptT (ExceptT), throwE)
 import Distribution.PackageDescription (BuildInfo (..), GenericPackageDescription (..))
-#if MIN_VERSION_Cabal(3,8,0)
-import Distribution.Simple.PackageDescription (readGenericPackageDescription)
-#else
-import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
-#endif
 import Distribution.Types.Benchmark (benchmarkBuildInfo)
 import Distribution.Types.CondTree (CondTree (..))
 import qualified Distribution.Types.Executable as Executable
@@ -17,7 +10,6 @@
 import Distribution.Utils.Path (getSymbolicPath)
 import qualified Distribution.Verbosity as Cabal
 import Exon (exon)
-import Hix.Data.Error (Error (..), pathText, sourceError)
 import Path (
   Abs,
   Dir,
@@ -35,6 +27,9 @@
   )
 import System.FilePattern.Directory (getDirectoryFiles)
 import System.IO.Error (tryIOError)
+
+import Hix.Compat (readGenericPackageDescription)
+import Hix.Data.Error (Error (..), pathText, sourceError)
 
 noMatch :: Text -> Path b File -> ExceptT Error IO a
 noMatch reason source =
diff --git a/lib/Hix/Compat.hs b/lib/Hix/Compat.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Compat.hs
@@ -0,0 +1,18 @@
+{-# language CPP #-}
+
+module Hix.Compat (
+  parseString,
+  readGenericPackageDescription,
+) where
+
+#if MIN_VERSION_Cabal(3,8,0)
+import Distribution.Simple.PackageDescription (readGenericPackageDescription)
+#else
+import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
+#endif
+
+#if MIN_VERSION_Cabal(3,8,0)
+import Distribution.Simple.PackageDescription (parseString)
+#else
+import Distribution.Fields.ParseResult (parseString)
+#endif
diff --git a/lib/Hix/Component.hs b/lib/Hix/Component.hs
--- a/lib/Hix/Component.hs
+++ b/lib/Hix/Component.hs
@@ -8,9 +8,8 @@
 import Exon (exon)
 import Path (Abs, Dir, File, Path, Rel, SomeBase (Abs, Rel), isProperPrefixOf, stripProperPrefix)
 
-import Hix.Data.Error (pathText)
-import qualified Hix.Data.GhciConfig as GhciConfig
-import Hix.Data.GhciConfig (
+import qualified Hix.Data.ComponentConfig
+import Hix.Data.ComponentConfig (
   ComponentConfig,
   PackageConfig (PackageConfig),
   PackageName (PackageName),
@@ -18,6 +17,7 @@
   SourceDir (SourceDir),
   Target (Target),
   )
+import Hix.Data.Error (pathText)
 import qualified Hix.Monad as Monad
 import Hix.Monad (Env (Env), M, noteEnv)
 import qualified Hix.Options as Options
diff --git a/lib/Hix/Data/BootstrapProjectConfig.hs b/lib/Hix/Data/BootstrapProjectConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Data/BootstrapProjectConfig.hs
@@ -0,0 +1,9 @@
+module Hix.Data.BootstrapProjectConfig where
+
+import Hix.Data.NewProjectConfig (HixUrl)
+
+data BootstrapProjectConfig =
+  BootstrapProjectConfig {
+    hixUrl :: HixUrl
+  }
+  deriving stock (Eq, Show, Generic)
diff --git a/lib/Hix/Data/ComponentConfig.hs b/lib/Hix/Data/ComponentConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Data/ComponentConfig.hs
@@ -0,0 +1,108 @@
+module Hix.Data.ComponentConfig where
+
+import Data.Aeson (FromJSON (parseJSON), FromJSONKey, withObject, (.:))
+import Path (Abs, Dir, File, Path, Rel)
+
+newtype PackagePath =
+  PackagePath { unPackagePath :: Path Rel Dir }
+  deriving stock (Eq, Show, Ord, Generic)
+  deriving newtype (FromJSON, FromJSONKey)
+
+newtype SourceDir =
+  SourceDir { unSourceDir :: Path Rel Dir }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromJSON)
+
+newtype SourceDirs =
+  SourceDirs { unSourceDirs :: [SourceDir] }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON SourceDirs where
+  parseJSON v =
+    (SourceDirs <$> parseJSON v)
+    <|>
+    (SourceDirs . pure <$> parseJSON v)
+
+newtype PackageName =
+  PackageName { unPackageName :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
+
+newtype ModuleName =
+  ModuleName { unModuleName :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
+
+newtype ComponentName =
+  ComponentName { unComponentName :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
+
+newtype EnvName =
+  EnvName { unEnvName :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
+
+newtype EnvRunner =
+  EnvRunner (Path Abs File)
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromJSON)
+
+data PreludePackage =
+  PreludePackageName Text
+  |
+  PreludePackageSpec { name :: Text }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON PreludePackage where
+  parseJSON v =
+    hpackStruct v <|> plainName
+    where
+      hpackStruct = withObject "PreludePackageSpec" \ o -> o .: "name"
+      plainName = PreludePackageName <$> parseJSON v
+
+data PreludeConfig =
+  PreludeConfig {
+    package :: PreludePackage,
+    module_ :: ModuleName
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON PreludeConfig where
+  parseJSON =
+    withObject "PreludeConfig" \ o -> do
+      package <- o .: "package"
+      module_ <- o .: "module"
+      pure PreludeConfig {..}
+
+data ComponentConfig =
+  ComponentConfig {
+    name :: ComponentName,
+    sourceDirs :: SourceDirs,
+    runner :: Maybe EnvRunner,
+    extensions :: [String],
+    language :: String,
+    ghcOptions :: [String],
+    prelude :: Maybe PreludeConfig
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (FromJSON)
+
+data PackageConfig =
+  PackageConfig {
+    name :: PackageName,
+    src :: Path Rel Dir,
+    components :: Map ComponentName ComponentConfig
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (FromJSON)
+
+type PackagesConfig = Map PackageName PackageConfig
+
+data Target =
+  Target {
+    package :: PackageConfig,
+    component :: ComponentConfig,
+    sourceDir :: Maybe SourceDir
+  }
+  deriving stock (Eq, Show, Generic)
diff --git a/lib/Hix/Data/Error.hs b/lib/Hix/Data/Error.hs
--- a/lib/Hix/Data/Error.hs
+++ b/lib/Hix/Data/Error.hs
@@ -14,6 +14,10 @@
   |
   GhciError Text
   |
+  NewError Text
+  |
+  BootstrapError Text
+  |
   NoMatch Text
   |
   Fatal Text
@@ -43,6 +47,20 @@
   m ()
 printGhciError msg =
   liftIO (Text.hPutStrLn stderr [exon|>>> Invalid ghci config: #{msg}|])
+
+printNewError ::
+  MonadIO m =>
+  Text ->
+  m ()
+printNewError msg =
+  liftIO (Text.hPutStrLn stderr [exon|>>> Can't create new project: #{msg}|])
+
+printBootstrapError ::
+  MonadIO m =>
+  Text ->
+  m ()
+printBootstrapError msg =
+  liftIO (Text.hPutStrLn stderr [exon|>>> Can't bootstrap project: #{msg}|])
 
 printFatalError ::
   MonadIO m =>
diff --git a/lib/Hix/Data/GhciConfig.hs b/lib/Hix/Data/GhciConfig.hs
--- a/lib/Hix/Data/GhciConfig.hs
+++ b/lib/Hix/Data/GhciConfig.hs
@@ -1,110 +1,9 @@
 module Hix.Data.GhciConfig where
 
-import Data.Aeson (FromJSON (parseJSON), FromJSONKey, withObject, (.:))
+import Data.Aeson (FromJSON, FromJSONKey)
 import GHC.Exts (IsList)
-import Path (Abs, Dir, File, Path, Rel)
 
-newtype PackagePath =
-  PackagePath { unPackagePath :: Path Rel Dir }
-  deriving stock (Eq, Show, Ord, Generic)
-  deriving newtype (FromJSON, FromJSONKey)
-
-newtype SourceDir =
-  SourceDir { unSourceDir :: Path Rel Dir }
-  deriving stock (Eq, Show, Generic)
-  deriving newtype (FromJSON)
-
-newtype SourceDirs =
-  SourceDirs { unSourceDirs :: [SourceDir] }
-  deriving stock (Eq, Show, Generic)
-
-instance FromJSON SourceDirs where
-  parseJSON v =
-    (SourceDirs <$> parseJSON v)
-    <|>
-    (SourceDirs . pure <$> parseJSON v)
-
-newtype PackageName =
-  PackageName { unPackageName :: Text }
-  deriving stock (Eq, Show, Generic)
-  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
-
-newtype ModuleName =
-  ModuleName { unModuleName :: Text }
-  deriving stock (Eq, Show, Generic)
-  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
-
-newtype ComponentName =
-  ComponentName { unComponentName :: Text }
-  deriving stock (Eq, Show, Generic)
-  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
-
-newtype EnvName =
-  EnvName { unEnvName :: Text }
-  deriving stock (Eq, Show, Generic)
-  deriving newtype (IsString, Ord, FromJSON, FromJSONKey)
-
-newtype EnvRunner =
-  EnvRunner (Path Abs File)
-  deriving stock (Eq, Show, Generic)
-  deriving newtype (FromJSON)
-
-data PreludePackage =
-  PreludePackageName Text
-  |
-  PreludePackageSpec { name :: Text }
-  deriving stock (Eq, Show, Generic)
-
-instance FromJSON PreludePackage where
-  parseJSON v =
-    hpackStruct v <|> plainName
-    where
-      hpackStruct = withObject "PreludePackageSpec" \ o -> o .: "name"
-      plainName = PreludePackageName <$> parseJSON v
-
-data PreludeConfig =
-  PreludeConfig {
-    package :: PreludePackage,
-    module_ :: ModuleName
-  }
-  deriving stock (Eq, Show, Generic)
-
-instance FromJSON PreludeConfig where
-  parseJSON =
-    withObject "PreludeConfig" \ o -> do
-      package <- o .: "package"
-      module_ <- o .: "module"
-      pure PreludeConfig {..}
-
-data ComponentConfig =
-  ComponentConfig {
-    name :: ComponentName,
-    sourceDirs :: SourceDirs,
-    runner :: Maybe EnvRunner,
-    extensions :: [String],
-    language :: String,
-    ghcOptions :: [String],
-    prelude :: Maybe PreludeConfig
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (FromJSON)
-
-data PackageConfig =
-  PackageConfig {
-    name :: PackageName,
-    src :: Path Rel Dir,
-    components :: Map ComponentName ComponentConfig
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (FromJSON)
-
-data Target =
-  Target {
-    package :: PackageConfig,
-    component :: ComponentConfig,
-    sourceDir :: Maybe SourceDir
-  }
-  deriving stock (Eq, Show, Generic)
+import Hix.Data.ComponentConfig (EnvRunner, PackagesConfig)
 
 newtype RunnerName =
   RunnerName { unRunnerName :: Text }
@@ -126,8 +25,6 @@
   deriving stock (Eq, Show, Generic)
   deriving newtype (IsList, Ord, FromJSON)
 
-type PackagesConfig = Map PackageName PackageConfig
-
 data EnvConfig =
   EnvConfig {
     packages :: PackagesConfig,
@@ -142,13 +39,6 @@
     setup :: Map RunnerName GhciSetupCode,
     run :: Map RunnerName GhciRunExpr,
     args :: GhciArgs
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (FromJSON)
-
-data PreprocConfig =
-  PreprocConfig {
-    packages :: PackagesConfig
   }
   deriving stock (Eq, Show, Generic)
   deriving anyclass (FromJSON)
diff --git a/lib/Hix/Data/NewProjectConfig.hs b/lib/Hix/Data/NewProjectConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Data/NewProjectConfig.hs
@@ -0,0 +1,28 @@
+module Hix.Data.NewProjectConfig where
+
+newtype ProjectName =
+  ProjectName { unProjectName :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord)
+
+newtype HixUrl =
+  HixUrl { unHixUrl :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord)
+
+instance Default HixUrl where
+  def = "github:tek/hix"
+
+newtype Author =
+  Author { unAuthor :: Text }
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (IsString, Ord)
+
+data NewProjectConfig =
+  NewProjectConfig {
+    name :: ProjectName,
+    packages :: Bool,
+    hixUrl :: HixUrl,
+    author :: Author
+  }
+  deriving stock (Eq, Show, Generic)
diff --git a/lib/Hix/Data/PreprocConfig.hs b/lib/Hix/Data/PreprocConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Data/PreprocConfig.hs
@@ -0,0 +1,12 @@
+module Hix.Data.PreprocConfig where
+
+import Data.Aeson (FromJSON)
+
+import Hix.Data.ComponentConfig (PackagesConfig)
+
+data PreprocConfig =
+  PreprocConfig {
+    packages :: PackagesConfig
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (FromJSON)
diff --git a/lib/Hix/Data/ProjectFile.hs b/lib/Hix/Data/ProjectFile.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Data/ProjectFile.hs
@@ -0,0 +1,27 @@
+module Hix.Data.ProjectFile where
+
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Reader (ask)
+import qualified Data.Text.IO as Text
+import Path (File, Path, Rel, parent, toFilePath, (</>))
+import Path.IO (createDirIfMissing)
+
+import Hix.Data.Error (tryIO)
+import qualified Hix.Monad
+import Hix.Monad (Env (Env), M)
+
+data ProjectFile =
+  ProjectFile {
+    path :: Path Rel File,
+    content :: Text
+  }
+  deriving stock (Eq, Show, Generic)
+
+createFile :: ProjectFile -> M ()
+createFile f = do
+  Env {root} <- ask
+  let
+    file = root </> f.path
+  lift $ tryIO do
+    createDirIfMissing True (parent file)
+    Text.writeFile (toFilePath file) f.content
diff --git a/lib/Hix/Env.hs b/lib/Hix/Env.hs
--- a/lib/Hix/Env.hs
+++ b/lib/Hix/Env.hs
@@ -3,9 +3,10 @@
 import qualified Data.Text.IO as Text
 
 import Hix.Component (targetComponent)
+import qualified Hix.Data.ComponentConfig
+import Hix.Data.ComponentConfig (EnvRunner (EnvRunner), PackagesConfig, Target (Target))
 import Hix.Data.Error (pathText)
-import qualified Hix.Data.GhciConfig as GhciConfig
-import Hix.Data.GhciConfig (EnvRunner (EnvRunner), PackagesConfig, Target (Target))
+import qualified Hix.Data.GhciConfig
 import Hix.Json (jsonConfig)
 import Hix.Monad (M)
 import qualified Hix.Options as Options
diff --git a/lib/Hix/Ghci.hs b/lib/Hix/Ghci.hs
--- a/lib/Hix/Ghci.hs
+++ b/lib/Hix/Ghci.hs
@@ -15,19 +15,18 @@
 import System.Posix.User (getLoginName)
 
 import Hix.Component (targetComponent)
-import Hix.Data.Error (Error, note, pathText, tryIO)
-import qualified Hix.Data.GhciConfig as GhciConfig
-import Hix.Data.GhciConfig (
+import qualified Hix.Data.ComponentConfig
+import Hix.Data.ComponentConfig (
   ComponentConfig,
-  GhciConfig,
-  GhciRunExpr (GhciRunExpr),
-  GhciSetupCode (GhciSetupCode),
   ModuleName (ModuleName),
   PackageConfig,
   PackageName,
   SourceDir (SourceDir),
   Target (Target),
   )
+import Hix.Data.Error (Error, note, pathText, tryIO)
+import qualified Hix.Data.GhciConfig
+import Hix.Data.GhciConfig (GhciConfig, GhciRunExpr (GhciRunExpr), GhciSetupCode (GhciSetupCode))
 import qualified Hix.Data.GhciTest as GhciTest
 import Hix.Data.GhciTest (GhciRun (GhciRun), GhciTest (GhciTest), GhcidRun (GhcidRun))
 import Hix.Json (jsonConfig)
diff --git a/lib/Hix/Monad.hs b/lib/Hix/Monad.hs
--- a/lib/Hix/Monad.hs
+++ b/lib/Hix/Monad.hs
@@ -5,7 +5,7 @@
 import Control.Monad.Trans.Reader (ReaderT (runReaderT))
 import Path (Abs, Dir, Path)
 
-import Hix.Data.Error (Error (EnvError, GhciError), tryIO)
+import Hix.Data.Error (Error (BootstrapError, EnvError, GhciError, NewError), tryIO)
 
 data Env =
   Env {
@@ -22,6 +22,14 @@
 noteGhci :: Text -> Maybe a -> M a
 noteGhci err =
   maybe (lift (throwE (GhciError err))) pure
+
+noteNew :: Text -> Maybe a -> M a
+noteNew err =
+  maybe (lift (throwE (NewError err))) pure
+
+noteBootstrap :: Text -> Maybe a -> M a
+noteBootstrap err =
+  maybe (lift (throwE (BootstrapError err))) pure
 
 runM :: Path Abs Dir -> M a -> IO (Either Error a)
 runM root ma =
diff --git a/lib/Hix/New.hs b/lib/Hix/New.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/New.hs
@@ -0,0 +1,176 @@
+module Hix.New where
+
+import Exon (exon)
+import Path (parseRelDir, parseRelFile, reldir, relfile, (</>))
+import Text.Casing (pascal)
+
+import qualified Hix.Data.NewProjectConfig
+import Hix.Data.NewProjectConfig (
+  Author,
+  HixUrl (HixUrl),
+  NewProjectConfig (NewProjectConfig),
+  ProjectName (ProjectName),
+  )
+import qualified Hix.Data.ProjectFile
+import Hix.Data.ProjectFile (ProjectFile (ProjectFile), createFile)
+import Hix.Monad (M, noteEnv)
+
+license :: Author -> Text
+license author =
+  [exon|Copyright (c) 2023 ##{author}
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+  disclaimer.
+  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+  disclaimer in the documentation and/or other materials provided with the distribution.
+
+Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those
+receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except
+for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell,
+import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired
+or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
+
+  (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of
+  contributors, in source or binary form) alone; or
+  (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such
+  copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to
+  be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
+
+Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this
+license, whether expressly, by implication, estoppel or otherwise.
+
+DISCLAIMER
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+|]
+
+flake :: NewProjectConfig -> Text
+flake NewProjectConfig {name = ProjectName name, hixUrl = HixUrl url, ..} =
+  [exon|{
+  description = "A Haskell project";
+
+  inputs.hix.url = "#{url}";
+
+  outputs = {hix, ...}: hix.lib.flake {
+    hackage.versionFile = "ops/version.nix";
+
+    cabal = {
+      license = "BSD-2-Clause-Patent";
+      license-file = "LICENSE";
+      author = "##{author}";
+      ghc-options = ["-Wall"];
+    };
+
+    packages.#{name} = {
+      src = ./#{src};
+      cabal.meta.synopsis = "A Haskell project";
+
+      library = {
+        enable = true;
+        dependencies = [
+          "containers"
+        ];
+      };
+
+      executable.enable = true;
+
+      test = {
+        enable = true;
+        dependencies = [
+          "hedgehog >= 1.1 && < 1.3"
+          "tasty ^>= 1.4"
+          "tasty-hedgehog >= 1.3 && < 1.5"
+        ];
+      };
+
+    };
+  };
+}
+|]
+  where
+    src | packages = [exon|packages/#{name}|]
+        | otherwise = "."
+
+libModule :: NewProjectConfig -> Text -> Text
+libModule conf modName =
+  [exon|module #{modName} where
+
+name :: String
+name = "#{conf.name.unProjectName}"
+|]
+
+appMainModule :: Text -> Text
+appMainModule modName =
+  [exon|module Main where
+
+import #{modName} (name)
+
+main :: IO ()
+main = putStrLn ("Hello " <> name)
+|]
+
+testMainModule :: Text -> Text
+testMainModule modName =
+  [exon|module Main where
+
+import Hedgehog (property, test, withTests)
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.Hedgehog (testProperty)
+import #{modName}.Test.NameTest (test_name)
+
+tests :: TestTree
+tests =
+  testGroup "all" [
+    testProperty "name" (withTests 1 (property (test test_name)))
+  ]
+
+main :: IO ()
+main = defaultMain tests
+|]
+
+nameTestModule :: NewProjectConfig -> Text -> Text
+nameTestModule conf modName =
+  [exon|module #{modName}.Test.NameTest where
+
+import Hedgehog (TestT, (===))
+
+import #{modName} (name)
+
+test_name :: TestT IO ()
+test_name = "#{conf.name.unProjectName}" === name
+|]
+
+newProjectFiles :: NewProjectConfig -> M [ProjectFile]
+newProjectFiles conf = do
+  nameDir <- pathError (parseRelDir modNameS)
+  let packageDir = if conf.packages then [reldir|packages|] </> nameDir else [reldir|.|]
+  libFile <- pathError (parseRelFile [exon|#{modNameS}.hs|])
+  pure [
+    ProjectFile {path = [relfile|flake.nix|], content = flake conf},
+    ProjectFile {path = packageDir </> [relfile|LICENSE|], content = license conf.author},
+    ProjectFile {path = packageDir </> [relfile|ops/version.nix|], content = [exon|"0.1.0.0"|]},
+    ProjectFile {path = packageDir </> [reldir|lib|] </> libFile, content = libModule conf modName},
+    ProjectFile {path = packageDir </> [relfile|app/Main.hs|], content = appMainModule modName},
+    ProjectFile {path = packageDir </> [relfile|test/Main.hs|], content = testMainModule modName},
+    ProjectFile {
+      path = packageDir </> [reldir|test|] </> nameDir </> [relfile|Test/NameTest.hs|],
+      content = nameTestModule conf modName
+    }
+    ]
+  where
+    pathError = noteEnv "Can't convert project name to file path"
+    modName = toText modNameS
+    modNameS = pascal (toString conf.name.unProjectName)
+
+newProject :: NewProjectConfig -> M ()
+newProject conf = do
+  traverse_ createFile =<< newProjectFiles conf
diff --git a/lib/Hix/Options.hs b/lib/Hix/Options.hs
--- a/lib/Hix/Options.hs
+++ b/lib/Hix/Options.hs
@@ -28,17 +28,19 @@
 import Path (Abs, Dir, File, Path, SomeBase, parseRelDir, parseSomeDir)
 import Prelude hiding (Mod, mod)
 
-import Hix.Data.GhciConfig (
+import qualified Hix.Data.BootstrapProjectConfig
+import Hix.Data.BootstrapProjectConfig (BootstrapProjectConfig (BootstrapProjectConfig))
+import Hix.Data.ComponentConfig (
   ComponentName (ComponentName),
-  EnvConfig,
   EnvName,
-  GhciConfig,
   ModuleName,
   PackageName (PackageName),
-  PreprocConfig,
-  RunnerName,
   SourceDir (SourceDir),
   )
+import Hix.Data.GhciConfig (EnvConfig, GhciConfig, RunnerName)
+import qualified Hix.Data.NewProjectConfig
+import Hix.Data.NewProjectConfig (NewProjectConfig (NewProjectConfig))
+import Hix.Data.PreprocConfig (PreprocConfig)
 import Hix.Optparse (JsonConfig, absFileOption, jsonOption)
 
 data PreprocOptions =
@@ -100,6 +102,18 @@
   }
   deriving stock (Show, Generic)
 
+data NewOptions =
+  NewOptions {
+    config :: NewProjectConfig
+  }
+  deriving stock (Eq, Show, Generic)
+
+data BootstrapOptions =
+  BootstrapOptions {
+    config :: BootstrapProjectConfig
+  }
+  deriving stock (Eq, Show, Generic)
+
 data EnvRunnerCommandOptions =
   EnvRunnerCommandOptions {
     options :: EnvRunnerOptions,
@@ -115,6 +129,10 @@
   GhcidCmd GhciOptions
   |
   GhciCmd GhciOptions
+  |
+  NewCmd NewOptions
+  |
+  BootstrapCmd BootstrapOptions
   deriving stock (Show)
 
 data GlobalOptions =
@@ -232,6 +250,19 @@
   test <- testOptionsParser
   pure GhciOptions {..}
 
+newParser :: Parser NewOptions
+newParser = do
+  name <- strOption (long "name" <> short 'n' <> help "The name of the new project and its main package")
+  packages <- switch (long "packages" <> short 'p' <> help "Store packages in the 'packages/' subdirectory")
+  hixUrl <- strOption (long "hix-url" <> help "The URL to the Hix repository" <> value def)
+  author <- strOption (long "author" <> short 'a' <> help "Your name")
+  pure NewOptions {config = NewProjectConfig {..}}
+
+bootstrapParser :: Parser BootstrapOptions
+bootstrapParser = do
+  hixUrl <- strOption (long "hix-url" <> help "The URL to the Hix repository" <> value def)
+  pure BootstrapOptions {config = BootstrapProjectConfig {..}}
+
 commands ::
   Mod CommandFields Command
 commands =
@@ -242,6 +273,10 @@
   command "ghci-cmd" (GhciCmd <$> info ghciParser (progDesc "Print a ghci cmdline to load a module in a Hix env"))
   <>
   command "ghcid-cmd" (GhcidCmd <$> info ghciParser (progDesc "Print a ghcid cmdline to run a function in a Hix env"))
+  <>
+  command "new" (NewCmd <$> info newParser (progDesc "Create a new Hix project in the current directory"))
+  <>
+  command "bootstrap" (BootstrapCmd <$> info bootstrapParser (progDesc "Bootstrap an existing Cabal project in the current directory"))
 
 globalParser :: Parser GlobalOptions
 globalParser = do
diff --git a/lib/Hix/Prelude.hs b/lib/Hix/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/lib/Hix/Prelude.hs
@@ -0,0 +1,39 @@
+module Hix.Prelude where
+
+import Data.Generics.Labels ()
+import Data.List.Extra (firstJust)
+import qualified Distribution.ModuleName as ModuleName
+import Distribution.ModuleName (ModuleName)
+import Distribution.PackageDescription (ModuleRenaming (..), unPackageName)
+import Distribution.Types.IncludeRenaming (IncludeRenaming (..))
+import Distribution.Types.Mixin (Mixin (..))
+import qualified Exon
+
+data Prelude =
+  Prelude {
+    preludePackage :: String,
+    preludeModule :: String
+  }
+  deriving stock (Eq, Show)
+
+preludeRenaming :: [(b, ModuleName)] -> Maybe b
+preludeRenaming =
+  firstJust \case
+    (real, "Prelude") -> Just real
+    _ -> Nothing
+
+pattern PreludeRenaming :: ModuleName -> ModuleRenaming
+pattern PreludeRenaming p <- ModuleRenaming (preludeRenaming -> Just p)
+
+pattern PreludeInclude :: ModuleName -> IncludeRenaming
+pattern PreludeInclude p <- IncludeRenaming {includeProvidesRn = PreludeRenaming p}
+
+findPrelude :: [Mixin] -> Maybe Prelude
+findPrelude =
+  firstJust \case
+    Mixin {mixinIncludeRenaming = PreludeInclude real, ..} ->
+      let
+        preludePackage = unPackageName mixinPackageName
+        preludeModule = Exon.intercalate "." (ModuleName.components real)
+      in Just Prelude {..}
+    _ -> Nothing
diff --git a/lib/Hix/Preproc.hs b/lib/Hix/Preproc.hs
--- a/lib/Hix/Preproc.hs
+++ b/lib/Hix/Preproc.hs
@@ -9,13 +9,8 @@
 import qualified Data.ByteString.Builder as ByteStringBuilder
 import Data.ByteString.Builder (Builder, byteString, charUtf8, stringUtf8)
 import Data.Generics.Labels ()
-import Data.List.Extra (firstJust)
-import qualified Distribution.ModuleName as ModuleName
-import Distribution.ModuleName (ModuleName)
-import Distribution.PackageDescription (BuildInfo (..), ModuleRenaming (..), unPackageName)
+import Distribution.PackageDescription (BuildInfo (..))
 import Distribution.Simple (PerCompilerFlavor (PerCompilerFlavor))
-import Distribution.Types.IncludeRenaming (IncludeRenaming (..))
-import Distribution.Types.Mixin (Mixin (..))
 import qualified Exon
 import Exon (exon)
 import Language.Haskell.Extension (
@@ -28,26 +23,23 @@
 
 import Hix.Cabal (buildInfoForFile)
 import Hix.Component (targetComponent)
+import qualified Hix.Data.ComponentConfig
+import Hix.Data.ComponentConfig (PreludeConfig, PreludePackage (PreludePackageName, PreludePackageSpec))
 import Hix.Data.Error (Error (..), sourceError, tryIO)
-import qualified Hix.Data.GhciConfig
-import Hix.Data.GhciConfig (PreludeConfig, PreludePackage (PreludePackageName, PreludePackageSpec), PreprocConfig)
+import qualified Hix.Data.PreprocConfig
+import Hix.Data.PreprocConfig (PreprocConfig)
 import Hix.Json (jsonConfig)
 import Hix.Monad (M)
 import Hix.Options (PreprocOptions (..), TargetSpec (TargetForFile))
 import Hix.Optparse (JsonConfig)
+import qualified Hix.Prelude as Prelude
+import Hix.Prelude (Prelude (Prelude), findPrelude)
 
 type Regex = IndexedTraversal' Int ByteString Match
 
-data Prelude =
-  Prelude {
-    preludePackage :: ByteString,
-    preludeModule :: ByteString
-  }
-  deriving stock (Show)
-
 fromPreludeConfig :: PreludeConfig -> Prelude
 fromPreludeConfig conf =
-  Prelude (encodeUtf8 (name conf.package)) (encodeUtf8 conf.module_.unModuleName)
+  Prelude (toString (name conf.package)) (toString conf.module_.unModuleName)
   where
     name = \case
       PreludePackageName n -> n
@@ -219,9 +211,9 @@
     Nothing
   where
     insertPrelude =
-      (ix 0 .~ [exon|"#{preludePackage}" |])
+      (ix 0 .~ [exon|"#{encodeUtf8 preludePackage}" |])
       .
-      (ix 1 .~ preludeModule)
+      (ix 1 .~ encodeUtf8 preludeModule)
 
 parenRegex :: Regex
 parenRegex =
@@ -354,31 +346,9 @@
         ..
       }
 
-preludeRenaming :: [(b, ModuleName)] -> Maybe b
-preludeRenaming =
-  firstJust \case
-    (real, "Prelude") -> Just real
-    _ -> Nothing
-
-pattern PreludeRenaming :: ModuleName -> ModuleRenaming
-pattern PreludeRenaming p <- ModuleRenaming (preludeRenaming -> Just p)
-
-pattern PreludeInclude :: ModuleName -> IncludeRenaming
-pattern PreludeInclude p <- IncludeRenaming {includeProvidesRn = PreludeRenaming p}
-
-findPrelude :: [Mixin] -> Maybe Prelude
-findPrelude =
-  firstJust \case
-    Mixin {mixinIncludeRenaming = PreludeInclude real, ..} ->
-      let
-        preludePackage = encodeUtf8 (unPackageName mixinPackageName)
-        preludeModule = Exon.intercalate "." (encodeUtf8 <$> ModuleName.components real)
-      in Just Prelude {..}
-    _ -> Nothing
-
 customPreludeImport :: Prelude -> Builder
 customPreludeImport Prelude {..} =
-  lineB [exon|import "#{byteString preludePackage}" #{byteString preludeModule} as Prelude|]
+  lineB [exon|import "#{stringUtf8 preludePackage}" #{stringUtf8 preludeModule} as Prelude|]
 
 needPreludeExtensions :: PreludeAction -> Bool
 needPreludeExtensions = \case
diff --git a/test/Hix/Test/BootstrapTest.hs b/test/Hix/Test/BootstrapTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Hix/Test/BootstrapTest.hs
@@ -0,0 +1,172 @@
+module Hix.Test.BootstrapTest where
+
+import qualified Data.Text.IO as Text
+import Exon (exon)
+import Hedgehog (TestT, evalEither, (===))
+import Path (parent, reldir, relfile, toFilePath, (</>))
+import Path.IO (createDirIfMissing, withSystemTempDir)
+
+import Hix.Bootstrap (bootstrapFiles)
+import qualified Hix.Data.BootstrapProjectConfig
+import Hix.Data.BootstrapProjectConfig (BootstrapProjectConfig (BootstrapProjectConfig))
+import qualified Hix.Data.ProjectFile as ProjectFile
+import Hix.Monad (runM)
+import qualified Data.Text as Text
+
+cabal :: Text
+cabal =
+  [exon|cabal-version: 2.2
+
+name:           red-panda
+version:        0.1.0.0
+synopsis:       A Haskell project
+description:    See https://hackage.haskell.org/package/red-panda/docs/RedPanda.html
+author:         Panda
+maintainer:     Panda
+license:        BSD-2-Clause-Patent
+license-file:   LICENSE
+build-type:     Simple
+
+library
+  exposed-modules:
+      RedPanda
+  other-modules:
+      Paths_red_panda
+  autogen-modules:
+      Paths_red_panda
+  hs-source-dirs:
+      lib
+  ghc-options: -Wall
+  build-depends:
+      base ==4.*
+    , incipit-base >= 0.4 && < 0.6
+    , containers
+  default-language: Haskell2010
+  default-extensions:
+      OverloadedStrings
+    , OverloadedRecordDot
+  mixins:
+      base hiding (Prelude)
+    , incipit-base (IncipitBase as Prelude)
+    , incipit-base hiding (IncipitBase)
+
+executable red-panda
+  main-is: Main.hs
+  other-modules:
+      Paths_red_panda
+  autogen-modules:
+      Paths_red_panda
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
+  build-depends:
+      base ==4.*
+    , red-panda
+  default-language: Haskell2010
+
+test-suite red-panda-test
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      RedPanda.Test.NameTest
+      Paths_red_panda
+  autogen-modules:
+      Paths_red_panda
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
+  build-depends:
+      base ==4.*
+    , hedgehog >=1.1 && <1.3
+    , red-panda
+    , tasty ==1.4.*
+    , tasty-hedgehog >=1.3 && <1.5
+  default-language: Haskell2010
+|]
+
+conf :: BootstrapProjectConfig
+conf =
+  BootstrapProjectConfig {hixUrl = def}
+
+flakeTarget :: Text
+flakeTarget =
+  [exon|{
+  description = "A Haskell project";
+  inputs.hix.url = "github:tek/hix";
+  outputs = {hix, ...}: hix.lib.flake {
+    packages = {
+      red-panda = {
+        src = ./packages/red-panda;
+        description = "See https://hackage.haskell.org/package/red-panda/docs/RedPanda.html";
+        cabal = {
+          author = "Panda";
+          build-type = "Simple";
+          license = "BSD-2-Clause-Patent";
+          license-file = "LICENSE";
+          meta = {
+            maintainer = "Panda";
+            synopsis = "A Haskell project";
+          };
+        };
+        library = {
+          dependencies = [
+            "containers"
+            "base >=4 && <5"
+          ];
+          default-extensions = [
+            "OverloadedStrings"
+            "OverloadedRecordDot"
+          ];
+          source-dirs = "lib";
+          language = "Haskell2010";
+          ghc-options = [
+            "-Wall"
+          ];
+          prelude = {
+            package = {
+              name = "incipit-base";
+              version = ">=0.4 && <0.6";
+            };
+            module = "IncipitBase";
+          };
+        };
+        executables.red-panda = {
+          dependencies = [
+            "red-panda"
+          ];
+          source-dirs = "app";
+          language = "Haskell2010";
+          ghc-options = [
+            "-Wall"
+          ];
+        };
+        tests.red-panda-test = {
+          dependencies = [
+            "hedgehog >=1.1 && <1.3"
+            "red-panda"
+            "tasty >=1.4 && <1.5"
+            "tasty-hedgehog >=1.3 && <1.5"
+          ];
+          source-dirs = "test";
+          language = "Haskell2010";
+          ghc-options = [
+            "-Wall"
+          ];
+        };
+      };
+    };
+  };
+}
+|]
+
+test_bootstrap :: TestT IO ()
+test_bootstrap = do
+  result <- liftIO $ withSystemTempDir "hix-unit" \ tmp -> do
+    let
+      root = tmp </> [reldir|red-panda|]
+      cabalFile = root </> [relfile|packages/red-panda/red-panda.cabal|]
+    createDirIfMissing True (parent cabalFile)
+    Text.writeFile (toFilePath cabalFile) cabal
+    runM root (bootstrapFiles conf)
+  [flake] <- evalEither result
+  Text.lines flakeTarget === Text.lines flake.content
diff --git a/test/Hix/Test/CabalFile.hs b/test/Hix/Test/CabalFile.hs
--- a/test/Hix/Test/CabalFile.hs
+++ b/test/Hix/Test/CabalFile.hs
@@ -1,16 +1,11 @@
-{-# language CPP #-}
-
 module Hix.Test.CabalFile where
 
-#if MIN_VERSION_Cabal(3,8,0)
-import Distribution.Simple.PackageDescription (parseString)
-#else
-import Distribution.Fields.ParseResult (parseString)
-#endif
 import Distribution.PackageDescription (GenericPackageDescription)
 import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
 import Distribution.Verbosity (silent)
 import Exon (exon)
+
+import Hix.Compat (parseString)
 
 testCabal :: ByteString
 testCabal =
diff --git a/test/Hix/Test/GhciTest.hs b/test/Hix/Test/GhciTest.hs
--- a/test/Hix/Test/GhciTest.hs
+++ b/test/Hix/Test/GhciTest.hs
@@ -6,18 +6,17 @@
 import Path (Abs, Dir, File, Path, Rel, SomeBase (Rel), absdir, absfile, reldir, relfile, (</>))
 import Path.IO (getCurrentDir, withSystemTempDir)
 
-import Hix.Data.Error (pathText)
-import Hix.Data.GhciConfig (
+import Hix.Data.ComponentConfig (
   ComponentConfig (..),
   ComponentName,
-  EnvConfig (EnvConfig),
   EnvRunner (EnvRunner),
-  GhciConfig (..),
   PackageConfig (..),
   PackagesConfig,
   SourceDir (SourceDir),
   SourceDirs (SourceDirs),
   )
+import Hix.Data.Error (pathText)
+import Hix.Data.GhciConfig (EnvConfig (EnvConfig), GhciConfig (..))
 import qualified Hix.Data.GhciTest as GhciTest
 import Hix.Env (envRunner)
 import Hix.Ghci (assemble, ghcidCmdlineFromOptions)
diff --git a/test/Hix/Test/NewTest.hs b/test/Hix/Test/NewTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Hix/Test/NewTest.hs
@@ -0,0 +1,125 @@
+module Hix.Test.NewTest where
+
+import Exon (exon)
+import Hedgehog (TestT, evalEither, (===))
+import Path (absdir, relfile)
+
+import qualified Hix.Data.NewProjectConfig
+import Hix.Data.NewProjectConfig (HixUrl, NewProjectConfig (NewProjectConfig))
+import Hix.Data.ProjectFile (ProjectFile (ProjectFile))
+import Hix.Monad (runM)
+import Hix.New (license, newProjectFiles)
+
+conf :: NewProjectConfig
+conf =
+  NewProjectConfig {name = "spider", packages = False, hixUrl = def, author = "Me"}
+
+flake :: Text
+flake =
+  [exon|{
+  description = "A Haskell project";
+
+  inputs.hix.url = "#{(def :: HixUrl).unHixUrl}";
+
+  outputs = {hix, ...}: hix.lib.flake {
+    hackage.versionFile = "ops/version.nix";
+
+    cabal = {
+      license = "BSD-2-Clause-Patent";
+      license-file = "LICENSE";
+      author = "Me";
+      ghc-options = ["-Wall"];
+    };
+
+    packages.spider = {
+      src = ./.;
+      cabal.meta.synopsis = "A Haskell project";
+
+      library = {
+        enable = true;
+        dependencies = [
+          "containers"
+        ];
+      };
+
+      executable.enable = true;
+
+      test = {
+        enable = true;
+        dependencies = [
+          "hedgehog >= 1.1 && < 1.3"
+          "tasty ^>= 1.4"
+          "tasty-hedgehog >= 1.3 && < 1.5"
+        ];
+      };
+
+    };
+  };
+}
+|]
+
+libModule :: Text
+libModule =
+  [exon|module Spider where
+
+name :: String
+name = "spider"
+|]
+
+appMainModule :: Text
+appMainModule =
+  [exon|module Main where
+
+import Spider (name)
+
+main :: IO ()
+main = putStrLn ("Hello " <> name)
+|]
+
+testMainModule :: Text
+testMainModule =
+  [exon|module Main where
+
+import Hedgehog (property, test, withTests)
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.Hedgehog (testProperty)
+import Spider.Test.NameTest (test_name)
+
+tests :: TestTree
+tests =
+  testGroup "all" [
+    testProperty "name" (withTests 1 (property (test test_name)))
+  ]
+
+main :: IO ()
+main = defaultMain tests
+|]
+
+nameTestModule :: Text
+nameTestModule =
+  [exon|module Spider.Test.NameTest where
+
+import Hedgehog (TestT, (===))
+
+import Spider (name)
+
+test_name :: TestT IO ()
+test_name = "spider" === name
+|]
+
+target :: [ProjectFile]
+target =
+  [
+    ProjectFile [relfile|flake.nix|] flake,
+    ProjectFile [relfile|LICENSE|] (license "Me"),
+    ProjectFile [relfile|ops/version.nix|] [exon|"0.1.0.0"|],
+    ProjectFile [relfile|lib/Spider.hs|] libModule,
+    ProjectFile [relfile|app/Main.hs|] appMainModule,
+    ProjectFile [relfile|test/Main.hs|] testMainModule,
+    ProjectFile [relfile|test/Spider/Test/NameTest.hs|] nameTestModule
+  ]
+
+test_new :: TestT IO ()
+test_new = do
+  res <- evalEither =<< liftIO (runM [absdir|/project|] (newProjectFiles conf))
+  target === res
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,8 +1,10 @@
 module Main where
 
 import Hedgehog (TestT, property, test, withTests)
+import Hix.Test.BootstrapTest (test_bootstrap)
 import Hix.Test.CabalTest (test_cabal)
 import Hix.Test.GhciTest (test_componentEnv, test_ghcid, test_moduleName)
+import Hix.Test.NewTest (test_new)
 import Hix.Test.PreprocTest (
   test_preprocInsertPrelude,
   test_preprocNoPrelude,
@@ -36,7 +38,9 @@
       unitTest "self exporting module, separate line" test_preprocNoPrelude,
       unitTest "run ghcid" test_ghcid,
       unitTest "component env" test_componentEnv,
-      unitTest "extract module name from path" test_moduleName
+      unitTest "extract module name from path" test_moduleName,
+      unitTest "generate a project" test_new,
+      unitTest "bootstrap a project" test_bootstrap
     ]
   ]
 
