diff --git a/cmd/hepto.go b/cmd/hepto.go
index 667a15ab3733d52a136b1e28decf6e8b2c5acfd2..f49e77e719dac04c5d107045031455ba67fcf766 100644
--- a/cmd/hepto.go
+++ b/cmd/hepto.go
@@ -1,86 +1,30 @@
 package main
 
 import (
-	"bytes"
-	"context"
 	"fmt"
-	"net/http"
-	"net/http/pprof"
 	"os"
 	"path/filepath"
-	"strings"
-	"syscall"
 
-	containerd "github.com/containerd/containerd/cmd/containerd/command"
-	ctr "github.com/containerd/containerd/cmd/ctr/app"
-	"github.com/containerd/containerd/plugin"
-	"github.com/containerd/containerd/runtime/v2/runc/manager"
-	_ "github.com/containerd/containerd/runtime/v2/runc/task/plugin"
-	shimv2 "github.com/containerd/containerd/runtime/v2/shim"
-	runc "github.com/opencontainers/runc/cmd"
 	"go.acides.org/hepto/cmd/hepto"
-	"golang.org/x/sys/unix"
-	kubectl "k8s.io/kubectl/pkg/cmd"
 )
 
 func main() {
-	bin := filepath.Base(os.Args[0])
-	// TODO move this to a separate function
-	// Enable pprof if required in the current or parent command line
-	template := "/proc/%d/cmdline"
-	cmdline, _ := os.ReadFile(fmt.Sprintf(template, syscall.Getpid()))
-	pcmdline, _ := os.ReadFile(fmt.Sprintf(template, syscall.Getppid()))
-	arg := []byte("--pprof")
-	if bytes.Contains(cmdline, arg) || bytes.Contains(pcmdline, arg) {
-		mux := http.NewServeMux()
-    for _, handler := range []string{"allocs", "blocks", "cmdline", "goroutine", "heap", "mutex", "profile", "threadcreate", "trace"} {
-      mux.Handle(fmt.Sprintf("/debug/pprof/%s", handler), pprof.Handler(handler))
-    }
-    mux.HandleFunc("/debug/pprof/", pprof.Index)
-		go http.ListenAndServe(":0", mux)
-	}
-	// Hook external execs to the single binary
-	var err error
-	if bin == "mount" {
-		// Hook the mount command for mounting configmaps
-		// This is fairly naive mount implementation, kubelet only evers calls
-		// mount with very simple very formatted arguments in that order:
-		//   mount -t tmpfs -o size=1234 /src /dst
-		err = unix.Mount(os.Args[5], os.Args[6], os.Args[2], 0, os.Args[4])
-	} else if bin == "umount" {
-		// Same for umount
-		err = unix.Unmount(os.Args[1], 0)
-	} else if bin == "containerd" || (len(os.Args) > 1 && os.Args[1] == "publish") {
-		// Containerd is also available under hepto name, guess based on
-		// call arguments
-		// This is some of an edge case, where containerd uses os.Executable
-		// to get the current binary path (hence hepto single binary) then
-		// passes that path as -publish-binary to its shim for callback
-		err = containerd.App().Run(os.Args)
-	} else if bin == "containerd-shim-runc-v2" || (len(os.Args) > 1 && os.Args[1] == "-namespace") {
-		// Run the containerd shim
-		// It is also available under hepto name, for similar reasons as
-		// containerd, hence the different guess condition
-		plugins := plugin.Graph(func(*plugin.Registration) bool { return false })
-		for _, plug := range plugins {
-			plug.Disable = !strings.HasPrefix(plug.URI(), "io.containerd.ttrpc")
+	// This is a multi-program  binary, pass the program to
+	// cobra so that it handles subcommands properly
+	program := filepath.Base(os.Args[0])
+	if len(os.Args) > 1 {
+		switch os.Args[1] {
+		case "publish":
+			program = hepto.Containerd.Use
+		case "-namespare":
+			program = hepto.Shim.Use
 		}
-		shimv2.RunManager(context.Background(), manager.NewShimManager("io.containerd.runc.v2"))
-	} else if bin == "runc" {
-		// Runc, as called by containerd shim
-		runc.Run()
-	} else if bin == "ctr" {
-		// Run containerd cli client, for debugging purposes
-		err = ctr.New().Run(os.Args)
-	} else if bin == "kubectl" {
-		// Run kubectl client, for debugging purposes
-		err = kubectl.NewDefaultKubectlCommand().Execute()
-	} else {
-		// If no hook ran a different command, simply run hepto
-		err = hepto.Hepto.Execute()
 	}
-	if err != nil {
-		fmt.Printf("unexpected error: %v", err)
+	// Use hepto as the default command in case no other matches
+	hepto.Hepto.Aliases = []string{program}
+	hepto.Root.SetArgs(append([]string{program}, os.Args[1:]...))
+	if err := hepto.Root.Execute(); err != nil {
+		fmt.Println(err)
 		os.Exit(1)
 	}
 }
diff --git a/cmd/hepto/config.go b/cmd/hepto/config.go
index 64836a8eb7e277edb0565d975831451adef02583..5c08097ce157f35cb9ae717057e9f3f3bc13302a 100644
--- a/cmd/hepto/config.go
+++ b/cmd/hepto/config.go
@@ -7,18 +7,13 @@ import (
 
 	"github.com/go-logr/logr"
 	"github.com/go-logr/zapr"
-	"github.com/spf13/cobra"
-	"github.com/spf13/pflag"
-	"github.com/spf13/viper"
 	"go.acides.org/hepto/pkg/cluster"
 	"go.acides.org/selfcontain"
-	"k8s.io/component-base/version/verflag"
 )
 
 type Config struct {
 	DataDir       string
 	BypassIPCheck bool
-	Shell         string
 	Pprof         bool
 	Logger        logr.Logger
 	LogLevel      int
@@ -30,13 +25,6 @@ type Config struct {
 var config Config
 
 func (c *Config) Complete() error {
-	// Print version if requested, verflag flags are declared
-	// by init() functions deep in k8s code
-	verflag.PrintAndExitIfRequested()
-	// Mount a shell if required
-	if c.Shell != "" {
-		c.Container.Mounts[c.Shell] = c.Shell
-	}
 	// Initialize logging, default to warn level
 	zapLogger, err := NewLogger(c.LogLevel)
 	if err != nil {
@@ -70,33 +58,3 @@ func (c *Config) Complete() error {
 	c.Container.Devices = additionalDevices
 	return nil
 }
-
-func init() {
-	// Hide unwanted flags declared inside k8s init() directly
-	pflag.CommandLine.MarkHidden("azure-container-registry-config")
-	cobra.OnInitialize(viper.AutomaticEnv)
-
-	// General config
-	Hepto.Flags().CountVarP(&config.LogLevel, "verbose", "v", "Make logs more verbose")
-	Hepto.Flags().StringVar(&config.Shell, "shell", "", "Path to a debug shell instead of hepto")
-	Hepto.Flags().BoolVar(&config.Pprof, "pprof", false, "Enable Golang pprof profiling")
-	Hepto.Flags().StringVar(&config.DataDir, "data", "/var/lib", "Data root directory")
-	Hepto.Flags().BoolVar(&config.BypassIPCheck, "bypass-ip-check", false, "Bypass initial IP check")
-
-	// Cluster settings
-	Hepto.Flags().StringVar(&config.Cluster.Name, "cluster", "hepto", "Hepto cluster name")
-	Hepto.Flags().BytesHexVar(&config.Cluster.Key, "key", []byte{}, "Main cluster 32bytes key, hex-encoded")
-
-	// Container settings
-	Hepto.Flags().StringVar(&config.Container.Master, "iface", "eth0", "Master network interface")
-	Hepto.Flags().IPVar(&config.Container.IP.IP, "ip", net.IP{}, "IP address for the public interface")
-	Hepto.Flags().IPVar(&config.Container.GW, "gw", net.IP{}, "IP address of the network gateway")
-	Hepto.Flags().IPSliceVar(&config.Container.DNS, "dns", defaultDNS, "DNS server IP addresses")
-	Hepto.Flags().StringToStringVar(&config.Container.Mounts, "bind", map[string]string{}, "Additional bind mounts")
-
-	// Node settings
-	Hepto.Flags().IntVar(&config.Node.Port, "discovery-port", 7123, "TCP port used for discovering the cluster")
-	Hepto.Flags().StringVar(&config.Node.Name, "name", "", "Hepto node name")
-	Hepto.Flags().StringSliceVar(&config.Node.Anchors, "anchors", []string{}, "List of cluster anchors")
-	Hepto.Flags().Var(&config.Node.Role, "role", "Node role inside the cluster")
-}
diff --git a/cmd/hepto/hooks.go b/cmd/hepto/hooks.go
new file mode 100644
index 0000000000000000000000000000000000000000..8edcca417f4341eb919d65ce152420d3af56f99c
--- /dev/null
+++ b/cmd/hepto/hooks.go
@@ -0,0 +1,87 @@
+package hepto
+
+import (
+	"context"
+	"os"
+	"strings"
+
+	containerd "github.com/containerd/containerd/cmd/containerd/command"
+	ctr "github.com/containerd/containerd/cmd/ctr/app"
+	"github.com/containerd/containerd/plugin"
+	"github.com/containerd/containerd/runtime/v2/runc/manager"
+	_ "github.com/containerd/containerd/runtime/v2/runc/task/plugin"
+	shimv2 "github.com/containerd/containerd/runtime/v2/shim"
+	runc "github.com/opencontainers/runc/cmd"
+	"github.com/spf13/cobra"
+	"golang.org/x/sys/unix"
+	kubectl "k8s.io/kubectl/pkg/cmd"
+)
+
+var Mount = &cobra.Command{
+	Use: "mount",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// Hook the mount command for mounting configmaps
+		// This is fairly naive mount implementation, kubelet only evers calls
+		// mount with very simple very formatted arguments in that order:
+		//   mount -t tmpfs -o size=1234 /src /dst
+		return unix.Mount(os.Args[5], os.Args[6], os.Args[2], 0, os.Args[4])
+	},
+}
+
+var Umount = &cobra.Command{
+	Use: "umount",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// Hook the umount command for mounting configmaps
+		return unix.Unmount(os.Args[1], 0)
+	},
+}
+
+var Containerd = &cobra.Command{
+	Use: "containerd",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// Containerd is also available under hepto name, guess based on
+		// call arguments
+		// This is some of an edge case, where containerd uses os.Executable
+		// to get the current binary path (hence hepto single binary) then
+		// passes that path as -publish-binary to its shim for callback
+		return containerd.App().Run(os.Args)
+	},
+}
+
+var Shim = &cobra.Command{
+	Use: "shim",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Run the containerd shim
+		// It is also available under hepto name, for similar reasons as
+		// containerd, hence the different guess condition
+		plugins := plugin.Graph(func(*plugin.Registration) bool { return false })
+		for _, plug := range plugins {
+			plug.Disable = !strings.HasPrefix(plug.URI(), "io.containerd.ttrpc")
+		}
+		shimv2.RunManager(context.Background(), manager.NewShimManager("io.containerd.runc.v2"))
+
+	},
+}
+
+var Runc = &cobra.Command{
+	Use: "runc",
+	Run: func(cmd *cobra.Command, args []string) {
+		runc.Run()
+	},
+}
+
+var Ctr = &cobra.Command{
+	Use: "ctr",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return ctr.New().Run(os.Args)
+
+	},
+}
+
+var Kubectl = &cobra.Command{
+	Use:                "kubectl",
+	DisableFlagParsing: true,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return kubectl.NewDefaultKubectlCommand().Execute()
+	},
+}
diff --git a/cmd/hepto/root.go b/cmd/hepto/root.go
index d78467dd1df26e00a7ccc61fce766fd95d440f51..d428aa4b7fbc71f00447cd93007a944e9bb98cf1 100644
--- a/cmd/hepto/root.go
+++ b/cmd/hepto/root.go
@@ -1,77 +1,22 @@
 package hepto
 
 import (
-	"fmt"
-	"net"
-	"os"
-	"os/exec"
-	"time"
-
 	"github.com/spf13/cobra"
-	"go.acides.org/hepto/pkg/cluster"
-	"go.acides.org/selfcontain"
 )
 
-var Hepto = &cobra.Command{
-	Use:   "hepto",
-	Short: "A highly opinionated geo-distributed Kubernetes distro",
-	Long: `Hepto is a Kubernetes distribution designed for geo-distributed
-	deployments, including across links with noticeable latency.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		config.Complete()
-		err := selfcontain.RunFun(&config.Container, func() {
-			cluster.Sysctl()
-		}, func() {
-			if config.Shell != "" {
-				cmd := exec.Command(config.Shell)
-				cmd.Stdin = os.Stdin
-				cmd.Stdout = os.Stdout
-				cmd.Stderr = os.Stderr
-				err := cmd.Run()
-				if err != nil {
-					config.Logger.Error(err, "could not run shell")
-				}
-				return
-			}
-			config.Node.IP = waitForIP()
-			config.Logger.Info("found current IP", "ip", config.Node.IP.String())
-			c := cluster.New(&config.Cluster, &config.Node)
-			c.Run()
-		})
-		if err != nil {
-			config.Logger.Error(err, "could not initialize the wrapping container")
-		}
-	},
+// This is not a proper cobra command, but a placeholder instead
+// for handling the multi-program behavior
+var Root = &cobra.Command{
+	// We use error returns for actual errors, not flag errors
+	SilenceUsage: true,
 }
 
-// Guess the current IP address
-func waitForIP() net.IP {
-	// This is very useful for debugging, especially in network isolated
-	// environments
-	if config.BypassIPCheck {
-		config.Logger.Info("bypassing IP check")
-		if len(config.Container.IP.IP) > 0 {
-			return config.Container.IP.IP
-		}
-		return net.ParseIP("::1")
-	}
-	// Otherwisem rely on opening a socket and probing the socket local
-	// address for more reliability
-	config.Logger.Info("determining current IP address")
-	for {
-		time.Sleep(time.Second)
-		target := fmt.Sprintf("[%s]:53", config.Container.DNS[0].String())
-		config.Logger.Info("connecting outbound to guess IP", "target", target)
-		conn, err := net.Dial("udp", target)
-		if err != nil {
-			config.Logger.Info("could not connect")
-			continue
-		}
-		localAddr := conn.LocalAddr().(*net.UDPAddr).IP
-		conn.Close()
-		if localAddr.IsLoopback() || localAddr.IsLinkLocalUnicast() {
-			continue
-		}
-		return localAddr
-	}
+func init() {
+	Root.AddCommand(Mount)
+	Root.AddCommand(Containerd)
+	Root.AddCommand(Shim)
+	Root.AddCommand(Runc)
+	Root.AddCommand(Kubectl)
+	Root.AddCommand(Ctr)
+	Root.AddCommand(Hepto)
 }
diff --git a/cmd/hepto/service.go b/cmd/hepto/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..42c3d02b29f3a04ed8896582d423167181962a3e
--- /dev/null
+++ b/cmd/hepto/service.go
@@ -0,0 +1,130 @@
+package hepto
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"time"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"github.com/spf13/viper"
+	"go.acides.org/hepto/pkg/cluster"
+	"go.acides.org/selfcontain"
+	"k8s.io/component-base/version/verflag"
+)
+
+var Hepto = &cobra.Command{
+	Use:   "hepto",
+	Short: "A highly opinionated geo-distributed Kubernetes distro",
+	Long: `Hepto is a Kubernetes distribution designed for geo-distributed
+	deployments, including across links with noticeable latency.`,
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		// Print version if requested, verflag flags are declared
+		// by init() functions deep in k8s code
+		verflag.PrintAndExitIfRequested()
+		// Complete the configuration
+		return config.Complete()
+	},
+}
+
+var Start = &cobra.Command{
+	Use:   "start",
+	Short: "Start the hepto service",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		cluster.Sysctl()
+		newArgs := append([]string{Run.Use}, os.Args[2:]...)
+		return selfcontain.RunWithArgs(&config.Container, newArgs)
+	},
+}
+
+var Run = &cobra.Command{
+	Use:   "run",
+	Short: "Actually run hepto inside the container",
+	// Determine current IP, which should run only once inside the container
+	PreRun: func(cmd *cobra.Command, args []string) {
+		// This is very useful for debugging, especially in network isolated
+		// environments
+		if config.BypassIPCheck {
+			config.Logger.Info("bypassing IP check")
+			if len(config.Container.IP.IP) > 0 {
+				config.Node.IP = config.Container.IP.IP
+			} else {
+				config.Node.IP = net.ParseIP("::1")
+			}
+			return
+		}
+		// Otherwisem rely on opening a socket and probing the socket local
+		// address for more reliability
+		config.Logger.Info("determining current IP address")
+		for {
+			time.Sleep(time.Second)
+			target := fmt.Sprintf("[%s]:53", config.Container.DNS[0].String())
+			config.Logger.Info("connecting outbound to guess IP", "target", target)
+			conn, err := net.Dial("udp", target)
+			if err != nil {
+				config.Logger.Info("could not connect")
+				continue
+			}
+			localAddr := conn.LocalAddr().(*net.UDPAddr).IP
+			conn.Close()
+			if localAddr.IsLoopback() || localAddr.IsLinkLocalUnicast() {
+				continue
+			}
+			config.Logger.Info("found current IP", "ip", config.Node.IP.String())
+			config.Node.IP = localAddr
+			break
+		}
+	},
+	Run: func(cmd *cobra.Command, args []string) {
+		c := cluster.New(&config.Cluster, &config.Node)
+		c.Run()
+	},
+}
+
+var RunKubectl = &cobra.Command{
+	Use:   "kubectl",
+	Short: "Run kubectl in the cluster",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		container, err := selfcontain.Get(&config.Container)
+		if err != nil {
+			return err
+		}
+		return container.Exec(append(
+			[]string{"/bin/kubectl", "--kubeconfig", "/root/.kube/config"},
+			args...,
+		))
+	},
+}
+
+func init() {
+	// Hide unwanted flags declared inside k8s init() directly
+	pflag.CommandLine.MarkHidden("azure-container-registry-config")
+	cobra.OnInitialize(viper.AutomaticEnv)
+	Hepto.AddCommand(Start)
+	Hepto.AddCommand(Run)
+	Hepto.AddCommand(RunKubectl)
+
+	// General config
+	Hepto.PersistentFlags().CountVarP(&config.LogLevel, "verbose", "v", "Make logs more verbose")
+	Hepto.PersistentFlags().BoolVar(&config.Pprof, "pprof", false, "Enable Golang pprof profiling")
+	Hepto.PersistentFlags().StringVar(&config.DataDir, "data", "/var/lib", "Data root directory")
+	Hepto.PersistentFlags().BoolVar(&config.BypassIPCheck, "bypass-ip-check", false, "Bypass initial IP check")
+
+	// Cluster settings
+	Hepto.PersistentFlags().StringVar(&config.Cluster.Name, "cluster", "hepto", "Hepto cluster name")
+	Hepto.PersistentFlags().BytesHexVar(&config.Cluster.Key, "key", []byte{}, "Main cluster 32bytes key, hex-encoded")
+
+	// Container settings
+	Hepto.PersistentFlags().StringVar(&config.Container.Master, "iface", "eth0", "Master network interface")
+	Hepto.PersistentFlags().IPVar(&config.Container.IP.IP, "ip", net.IP{}, "IP address for the public interface")
+	Hepto.PersistentFlags().IPVar(&config.Container.GW, "gw", net.IP{}, "IP address of the network gateway")
+	Hepto.PersistentFlags().IPSliceVar(&config.Container.DNS, "dns", defaultDNS, "DNS server IP addresses")
+	Hepto.PersistentFlags().StringToStringVar(&config.Container.Mounts, "bind", map[string]string{}, "Additional bind mounts")
+
+	// Node settings
+	Hepto.PersistentFlags().IntVar(&config.Node.Port, "discovery-port", 7123, "TCP port used for discovering the cluster")
+	Hepto.PersistentFlags().StringVar(&config.Node.Name, "name", "", "Hepto node name")
+	Hepto.PersistentFlags().StringSliceVar(&config.Node.Anchors, "anchors", []string{}, "List of cluster anchors")
+	Hepto.PersistentFlags().Var(&config.Node.Role, "role", "Node role inside the cluster")
+}
diff --git a/go.mod b/go.mod
index 1347bf5d82173cf893eb6267ffb26991f8fe38a4..02a16d43e596407b8cf7d15846cd5b182203aa03 100644
--- a/go.mod
+++ b/go.mod
@@ -71,7 +71,7 @@ require (
 	github.com/spf13/viper v1.15.0
 	github.com/vishvananda/netlink v1.2.1-beta.2
 	go.acides.org/pekahi v0.1.1
-	go.acides.org/selfcontain v0.1.1
+	go.acides.org/selfcontain v0.2.0
 	go.acides.org/sml v0.1.1
 	go.etcd.io/etcd/server/v3 v3.5.7
 	go.uber.org/zap v1.24.0
diff --git a/go.sum b/go.sum
index 8c16cadabc43cce725188a74d0c5fcc500c4039d..c45e7346436d199ee5d0dee07a948d5043ebb169 100644
--- a/go.sum
+++ b/go.sum
@@ -1326,8 +1326,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.acides.org/pekahi v0.1.1 h1:lohNKOhw9Fz5K1Q6K3tP7XFWc+d/O29D9AEXnY2EKU8=
 go.acides.org/pekahi v0.1.1/go.mod h1:AxgN7Ss6dCRHoNOVWMymkmDafWYdDV7ce6jPl5bqyRc=
-go.acides.org/selfcontain v0.1.1 h1:a3TvW2TaujF4NDnVpFd7D9QPfkrFBXvIXN3VAIneZh4=
-go.acides.org/selfcontain v0.1.1/go.mod h1:cyKYsVw1scp6MTVIhquG+2OJrsyaDCwkXlsBvMO+cws=
+go.acides.org/selfcontain v0.2.0 h1:7b9rfBIGOqpPjqIaXo2h8OZUeCu8+XrFh9zzToi2JKM=
+go.acides.org/selfcontain v0.2.0/go.mod h1:cyKYsVw1scp6MTVIhquG+2OJrsyaDCwkXlsBvMO+cws=
 go.acides.org/sml v0.1.1 h1:v424XQ1RhgZHfKG+rV0VZ8YL32HC1UlqbIqv4E4q5JU=
 go.acides.org/sml v0.1.1/go.mod h1:lBAbmfk5FFmK5yt7pFoqwwGNtEfWL/aXAdCj0ry7Kj0=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=