diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd01bc08bf1370bce26a947e9fee1a7a6b70cc96
--- /dev/null
+++ b/cmd/flux/bootstrap_git.go
@@ -0,0 +1,240 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"net/url"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/go-git/go-git/v5/plumbing/transport"
+	"github.com/go-git/go-git/v5/plumbing/transport/http"
+	"github.com/go-git/go-git/v5/plumbing/transport/ssh"
+	"github.com/manifoldco/promptui"
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/fluxcd/flux2/internal/bootstrap"
+	"github.com/fluxcd/flux2/internal/bootstrap/git/gogit"
+	"github.com/fluxcd/flux2/internal/flags"
+	"github.com/fluxcd/flux2/internal/utils"
+	"github.com/fluxcd/flux2/pkg/manifestgen/install"
+	"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
+	"github.com/fluxcd/flux2/pkg/manifestgen/sync"
+)
+
+var bootstrapGitCmd = &cobra.Command{
+	Use:   "git",
+	Short: "Bootstrap toolkit components in a Git repository",
+	Long: `The bootstrap git command commits the toolkit components manifests to the
+branch of a Git repository. It then configures the target cluster to synchronize with
+the repository. If the toolkit components are present on the cluster, the bootstrap
+command will perform an upgrade if needed.`,
+	Example: `  # Run bootstrap for a Git repository and authenticate with your SSH agent
+  flux bootstrap git --url=ssh://git@example.com/repository.git
+
+  # Run bootstrap for a Git repository and authenticate using a password
+  flux bootstrap git --url=https://example.com/repository.git --password=<password>
+
+  # Run bootstrap for a Git repository with a passwordless private key
+  flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key>
+`,
+	RunE: bootstrapGitCmdRun,
+}
+
+type gitFlags struct {
+	url      string
+	interval time.Duration
+	path     flags.SafeRelativePath
+	username string
+	password string
+}
+
+var gitArgs gitFlags
+
+func init() {
+	bootstrapGitCmd.Flags().StringVar(&gitArgs.url, "url", "", "Git repository URL")
+	bootstrapGitCmd.Flags().DurationVar(&gitArgs.interval, "interval", time.Minute, "sync interval")
+	bootstrapGitCmd.Flags().Var(&gitArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
+	bootstrapGitCmd.Flags().StringVarP(&gitArgs.username, "username", "u", "git", "basic authentication username")
+	bootstrapGitCmd.Flags().StringVarP(&gitArgs.password, "password", "p", "", "basic authentication password")
+
+	bootstrapCmd.AddCommand(bootstrapGitCmd)
+}
+
+func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
+	if err := bootstrapValidate(); err != nil {
+		return err
+	}
+
+	repositoryURL, err := url.Parse(gitArgs.url)
+	if err != nil {
+		return err
+	}
+	gitAuth, err := transportForURL(repositoryURL)
+	if err != nil {
+		return err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
+	defer cancel()
+
+	kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
+	if err != nil {
+		return err
+	}
+
+	// Manifest base
+	if ver, err := getVersion(bootstrapArgs.version); err == nil {
+		bootstrapArgs.version = ver
+	}
+	manifestsBase, err := buildEmbeddedManifestBase()
+	if err != nil {
+		return err
+	}
+	defer os.RemoveAll(manifestsBase)
+
+	// Lazy go-git repository
+	tmpDir, err := ioutil.TempDir("", "flux-bootstrap-")
+	if err != nil {
+		return fmt.Errorf("failed to create temporary working dir: %w", err)
+	}
+	defer os.RemoveAll(tmpDir)
+	gitClient := gogit.New(tmpDir, gitAuth)
+
+	// Install manifest config
+	installOptions := install.Options{
+		BaseURL:                rootArgs.defaults.BaseURL,
+		Version:                bootstrapArgs.version,
+		Namespace:              rootArgs.namespace,
+		Components:             bootstrapComponents(),
+		Registry:               bootstrapArgs.registry,
+		ImagePullSecret:        bootstrapArgs.imagePullSecret,
+		WatchAllNamespaces:     bootstrapArgs.watchAllNamespaces,
+		NetworkPolicy:          bootstrapArgs.networkPolicy,
+		LogLevel:               bootstrapArgs.logLevel.String(),
+		NotificationController: rootArgs.defaults.NotificationController,
+		ManifestFile:           rootArgs.defaults.ManifestFile,
+		Timeout:                rootArgs.timeout,
+		TargetPath:             gitArgs.path.String(),
+		ClusterDomain:          bootstrapArgs.clusterDomain,
+		TolerationKeys:         bootstrapArgs.tolerationKeys,
+	}
+	if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" {
+		installOptions.BaseURL = customBaseURL
+	}
+
+	// Source generation and secret config
+	secretOpts := sourcesecret.Options{
+		Name:         bootstrapArgs.secretName,
+		Namespace:    rootArgs.namespace,
+		TargetPath:   gitArgs.path.String(),
+		ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
+	}
+	if bootstrapArgs.tokenAuth {
+		secretOpts.Username = gitArgs.username
+		secretOpts.Password = gitArgs.password
+
+		if bootstrapArgs.caFile != "" {
+			secretOpts.CAFilePath = bootstrapArgs.caFile
+		}
+	} else {
+		secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
+		secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
+		secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
+		secretOpts.SSHHostname = repositoryURL.Host
+
+		if bootstrapArgs.sshHostname != "" {
+			secretOpts.SSHHostname = bootstrapArgs.sshHostname
+		}
+	}
+
+	// Sync manifest config
+	syncOpts := sync.Options{
+		Interval:          gitArgs.interval,
+		Name:              rootArgs.namespace,
+		Namespace:         rootArgs.namespace,
+		URL:               gitArgs.url,
+		Branch:            bootstrapArgs.branch,
+		Secret:            bootstrapArgs.secretName,
+		TargetPath:        gitArgs.path.String(),
+		ManifestFile:      sync.MakeDefaultOptions().ManifestFile,
+		GitImplementation: sourceGitArgs.gitImplementation.String(),
+	}
+
+	// Bootstrap config
+	bootstrapOpts := []bootstrap.GitOption{
+		bootstrap.WithRepositoryURL(gitArgs.url),
+		bootstrap.WithBranch(bootstrapArgs.branch),
+		bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
+		bootstrap.WithKubeconfig(rootArgs.kubeconfig, rootArgs.kubecontext),
+		bootstrap.WithPostGenerateSecretFunc(promptPublicKey),
+		bootstrap.WithLogger(logger),
+	}
+
+	// Setup bootstrapper with constructed configs
+	b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
+	if err != nil {
+		return err
+	}
+
+	// Run
+	return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
+}
+
+// transportForURL constructs a transport.AuthMethod based on the scheme
+// of the given URL and the configured flags. If the protocol equals
+// "ssh" but no private key is configured, authentication using the local
+// SSH-agent is attempted.
+func transportForURL(u *url.URL) (transport.AuthMethod, error) {
+	switch u.Scheme {
+	case "https":
+		return &http.BasicAuth{
+			Username: gitArgs.username,
+			Password: gitArgs.password,
+		}, nil
+	case "ssh":
+		if bootstrapArgs.privateKeyFile != "" {
+			return ssh.NewPublicKeysFromFile(u.User.Username(), bootstrapArgs.privateKeyFile, "")
+		}
+		return nil, nil
+	default:
+		return nil, fmt.Errorf("scheme %q is not supported", u.Scheme)
+	}
+}
+
+func promptPublicKey(ctx context.Context, secret corev1.Secret, _ sourcesecret.Options) error {
+	ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey]
+	if !ok {
+		return nil
+	}
+
+	logger.Successf("public key: %s", strings.TrimSpace(ppk))
+	prompt := promptui.Prompt{
+		Label:     "Please give the key access to your repository",
+		IsConfirm: true,
+	}
+	_, err := prompt.Run()
+	if err != nil {
+		return fmt.Errorf("aborting")
+	}
+	return nil
+}
diff --git a/docs/cmd/flux_bootstrap.md b/docs/cmd/flux_bootstrap.md
index 78760620a143f17ff1657fea2b7f64adc5a241da..d6ca280a5ff2990edb0e87cb0f9b81ca3d6b8975 100644
--- a/docs/cmd/flux_bootstrap.md
+++ b/docs/cmd/flux_bootstrap.md
@@ -49,6 +49,7 @@ The bootstrap sub-commands bootstrap the toolkit components on the targeted Git
 ### SEE ALSO
 
 * [flux](../flux/)	 - Command line utility for assembling Kubernetes CD pipelines
+* [flux bootstrap git](../flux_bootstrap_git/)	 - Bootstrap toolkit components in a Git repository
 * [flux bootstrap github](../flux_bootstrap_github/)	 - Bootstrap toolkit components in a GitHub repository
 * [flux bootstrap gitlab](../flux_bootstrap_gitlab/)	 - Bootstrap toolkit components in a GitLab repository
 
diff --git a/docs/cmd/flux_bootstrap_git.md b/docs/cmd/flux_bootstrap_git.md
new file mode 100644
index 0000000000000000000000000000000000000000..638199396e3513e1c9cd9d746cb686f478fea1f4
--- /dev/null
+++ b/docs/cmd/flux_bootstrap_git.md
@@ -0,0 +1,78 @@
+---
+title: "flux bootstrap git command"
+---
+## flux bootstrap git
+
+Bootstrap toolkit components in a Git repository
+
+### Synopsis
+
+The bootstrap git command commits the toolkit components manifests to the
+branch of a Git repository. It then configures the target cluster to synchronize with
+the repository. If the toolkit components are present on the cluster, the bootstrap
+command will perform an upgrade if needed.
+
+```
+flux bootstrap git [flags]
+```
+
+### Examples
+
+```
+  # Run bootstrap for a Git repository and authenticate with your SSH agent
+  flux bootstrap git --url=ssh://git@example.com/repository.git
+
+  # Run bootstrap for a Git repository and authenticate using a password
+  flux bootstrap git --url=https://example.com/repository.git --password=<password>
+
+  # Run bootstrap for a Git repository with a passwordless private key
+  flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key>
+
+```
+
+### Options
+
+```
+  -h, --help                    help for git
+      --interval duration       sync interval (default 1m0s)
+  -p, --password string         basic authentication password
+      --path safeRelativePath   path relative to the repository root, when specified the cluster sync will be scoped to this path
+      --url string              Git repository URL
+  -u, --username string         basic authentication username (default "git")
+```
+
+### Options inherited from parent commands
+
+```
+      --author-email string                    author email for Git commits
+      --author-name string                     author name for Git commits (default "Flux")
+      --branch string                          default branch (for GitHub this must match the default branch setting for the organization) (default "main")
+      --ca-file string                         path to TLS CA file used for validating self-signed certificates
+      --cluster-domain string                  internal cluster domain (default "cluster.local")
+      --components strings                     list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
+      --components-extra strings               list of components in addition to those supplied or defaulted, accepts comma-separated values
+      --context string                         kubernetes context to use
+      --image-pull-secret string               Kubernetes secret name used for pulling the toolkit images from a private registry
+      --kubeconfig string                      absolute path to the kubeconfig file
+      --log-level logLevel                     log level, available options are: (debug, info, error) (default info)
+  -n, --namespace string                       the namespace scope for this operation (default "flux-system")
+      --network-policy                         deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
+      --private-key-file string                path to a private key file used for authenticating to the Git SSH server
+      --registry string                        container registry where the toolkit images are published (default "ghcr.io/fluxcd")
+      --secret-name string                     name of the secret the sync credentials can be found in or stored to (default "flux-system")
+      --ssh-ecdsa-curve ecdsaCurve             SSH ECDSA public key curve (p256, p384, p521) (default p384)
+      --ssh-hostname string                    SSH hostname, to be used when the SSH host differs from the HTTPS one
+      --ssh-key-algorithm publicKeyAlgorithm   SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
+      --ssh-rsa-bits rsaKeyBits                SSH RSA public key bit size (multiplies of 8) (default 2048)
+      --timeout duration                       timeout for this operation (default 5m0s)
+      --token-auth                             when enabled, the personal access token will be used instead of SSH deploy key
+      --toleration-keys strings                list of toleration keys used to schedule the components pods onto nodes with matching taints
+      --verbose                                print generated objects
+  -v, --version string                         toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
+      --watch-all-namespaces                   watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
+```
+
+### SEE ALSO
+
+* [flux bootstrap](../flux_bootstrap/)	 - Bootstrap toolkit components
+