diff --git a/README.md b/README.md
index 298050b..0b91152 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,9 @@ Act runner is a runner for Forges supports Github Actions protocol.
 create a new file as name `.env`
 
 ```sh
-GITEA_RUNNER_NAME=foobar
-GITEA_RUNNER_URL=http://localhost:3000/foo/bar
+GITEA_RUNNER_NAME=
+GITEA_RPC_PROTO=http
+GITEA_RPC_HOST=localhost:3001
 GITEA_RUNNER_TOKEN=1234567890abcde
 GITEA_RUNNER_LABELS=foo,bar
 ```
diff --git a/cmd/daemon.go b/cmd/daemon.go
index 62c2fb8..4c58eb3 100644
--- a/cmd/daemon.go
+++ b/cmd/daemon.go
@@ -94,11 +94,6 @@ func runDaemon(ctx context.Context, task *runtime.Task) func(cmd *cobra.Command,
 				WithField("arch", cfg.Platform.Arch).
 				Infoln("polling the remote server")
 
-			// register new runner
-			if err := poller.Register(ctx, cfg.Runner); err != nil {
-				return err
-			}
-
 			// update runner status to idle
 			log.Infoln("update runner status to idle")
 			if _, err := cli.UpdateRunner(
diff --git a/cmd/register.go b/cmd/register.go
new file mode 100644
index 0000000..72aa280
--- /dev/null
+++ b/cmd/register.go
@@ -0,0 +1,88 @@
+package cmd
+
+import (
+	"context"
+	"time"
+
+	"gitea.com/gitea/act_runner/client"
+	"gitea.com/gitea/act_runner/config"
+	"gitea.com/gitea/act_runner/poller"
+	"gitea.com/gitea/act_runner/runtime"
+	pingv1 "gitea.com/gitea/proto-go/ping/v1"
+
+	"github.com/bufbuild/connect-go"
+	"github.com/joho/godotenv"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+func runRegister(ctx context.Context, task *runtime.Task) func(cmd *cobra.Command, args []string) error {
+	return func(cmd *cobra.Command, args []string) error {
+		log.Infoln("Starting runner daemon")
+
+		_ = godotenv.Load(task.Input.EnvFile)
+		cfg, err := config.FromEnviron()
+		if err != nil {
+			log.WithError(err).
+				Fatalln("invalid configuration")
+		}
+
+		initLogging(cfg)
+
+		cli := client.New(
+			cfg.Client.Address,
+			cfg.Client.Secret,
+			client.WithSkipVerify(cfg.Client.SkipVerify),
+			client.WithGRPC(cfg.Client.GRPC),
+			client.WithGRPCWeb(cfg.Client.GRPCWeb),
+		)
+
+		for {
+			_, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
+				Data: cfg.Runner.Name,
+			}))
+			select {
+			case <-ctx.Done():
+				return nil
+			default:
+			}
+			if ctx.Err() != nil {
+				break
+			}
+			if err != nil {
+				log.WithError(err).
+					Errorln("cannot ping the remote server")
+				// TODO: if ping failed, retry or exit
+				time.Sleep(time.Second)
+			} else {
+				log.Infoln("successfully connected the remote server")
+				break
+			}
+		}
+
+		runner := &runtime.Runner{
+			Client:  cli,
+			Machine: cfg.Runner.Name,
+			Environ: cfg.Runner.Environ,
+		}
+
+		poller := poller.New(
+			cli,
+			runner.Run,
+			&client.Filter{
+				OS:     cfg.Platform.OS,
+				Arch:   cfg.Platform.Arch,
+				Labels: cfg.Runner.Labels,
+			},
+		)
+
+		// register new runner
+		if err := poller.Register(ctx, cfg.Runner); err != nil {
+			return err
+		}
+
+		log.Infoln("successfully registered new runner")
+
+		return nil
+	}
+}
diff --git a/cmd/root.go b/cmd/root.go
index 09d0312..9fc3dda 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -52,12 +52,21 @@ func Execute(ctx context.Context) {
 	// ./act_runner daemon
 	daemonCmd := &cobra.Command{
 		Aliases: []string{"daemon"},
-		Use:     "daemon [event name to run]\nIf no event name passed, will default to \"on: push\"",
-		Short:   "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
+		Use:     "execute runner daemon",
 		Args:    cobra.MaximumNArgs(1),
 		RunE:    runDaemon(ctx, task),
 	}
-	rootCmd.AddCommand(daemonCmd)
+
+	// ./act_runner daemon
+	registerCmd := &cobra.Command{
+		Aliases: []string{"register"},
+		Use:     "register new runner",
+		Args:    cobra.MaximumNArgs(1),
+		RunE:    runRegister(ctx, task),
+	}
+
+	// add all command
+	rootCmd.AddCommand(daemonCmd, registerCmd)
 
 	if err := rootCmd.Execute(); err != nil {
 		os.Exit(1)
diff --git a/config/config.go b/config/config.go
index e41e46c..1e97a88 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,7 +2,6 @@ package config
 
 import (
 	"fmt"
-	"net/url"
 	"os"
 	"runtime"
 
@@ -32,19 +31,17 @@ type (
 
 	Runner struct {
 		Name     string            `envconfig:"GITEA_RUNNER_NAME"`
-		URL      string            `envconfig:"GITEA_RUNNER_URL" required:"true"`
 		Token    string            `envconfig:"GITEA_RUNNER_TOKEN" required:"true"`
 		Capacity int               `envconfig:"GITEA_RUNNER_CAPACITY" default:"1"`
+		File     string            `envconfig:"GITEA_RUNNER_FILE" default:".runner"`
 		Environ  map[string]string `envconfig:"GITEA_RUNNER_ENVIRON"`
 		EnvFile  string            `envconfig:"GITEA_RUNNER_ENV_FILE"`
 		Labels   []string          `envconfig:"GITEA_RUNNER_LABELS"`
 	}
 
 	Platform struct {
-		OS      string `envconfig:"GITEA_PLATFORM_OS"`
-		Arch    string `envconfig:"GITEA_PLATFORM_ARCH"`
-		Kernel  string `envconfig:"GITEA_PLATFORM_KERNEL"`
-		Variant string `envconfig:"GITEA_PLATFORM_VARIANT"`
+		OS   string `envconfig:"GITEA_PLATFORM_OS"`
+		Arch string `envconfig:"GITEA_PLATFORM_ARCH"`
 	}
 )
 
@@ -55,14 +52,6 @@ func FromEnviron() (Config, error) {
 		return cfg, err
 	}
 
-	// check runner remote url
-	u, err := url.Parse(cfg.Runner.URL)
-	if err != nil {
-		return cfg, err
-	}
-
-	cfg.Client.Proto = u.Scheme
-	cfg.Client.Host = u.Host
 	cfg.Client.Secret = cfg.Runner.Token
 
 	// runner config
@@ -97,5 +86,5 @@ func FromEnviron() (Config, error) {
 		}
 	}
 
-	return cfg, err
+	return cfg, nil
 }
diff --git a/core/runner.go b/core/runner.go
new file mode 100644
index 0000000..0ea6559
--- /dev/null
+++ b/core/runner.go
@@ -0,0 +1,9 @@
+package core
+
+// Runner struct
+type Runner struct {
+	ID    int64  `json:"id"`
+	UUID  string `json:"uuid"`
+	Name  string `json:"name"`
+	Token string `json:"token"`
+}
diff --git a/go.mod b/go.mod
index 75f02dc..4fb7716 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module gitea.com/gitea/act_runner
 go 1.18
 
 require (
-	gitea.com/gitea/proto-go v0.0.0-20221013073523-69d53451957a
+	gitea.com/gitea/proto-go v0.0.0-20221014123629-9116865c883b
 	github.com/appleboy/com v0.1.6
 	github.com/avast/retry-go/v4 v4.1.0
 	github.com/bufbuild/connect-go v0.5.0
diff --git a/go.sum b/go.sum
index 0f7d7bf..bb611a3 100644
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,8 @@ gitea.com/gitea/act v0.0.0-20220922135643-52a5bba9e7fa h1:HHqlvfIvqFlny3sgJgAM1B
 gitea.com/gitea/act v0.0.0-20220922135643-52a5bba9e7fa/go.mod h1:9W/Nz16tjfnWp7O5DUo3EjZBnZFBI/5rlWstX4o7+hU=
 gitea.com/gitea/proto-go v0.0.0-20221013073523-69d53451957a h1:WHNPcbDR2vw2a17Ml06+n4MC0UwsyD/F3WeVteaXWMI=
 gitea.com/gitea/proto-go v0.0.0-20221013073523-69d53451957a/go.mod h1:hD8YwSHusjwjEEgubW6XFvnZuNhMZTHz6lwjfltEt/Y=
+gitea.com/gitea/proto-go v0.0.0-20221014123629-9116865c883b h1:TSz7VRHfnM/5JwGPgIAjSlDIvcr4pTGfuRMtgMxttmg=
+gitea.com/gitea/proto-go v0.0.0-20221014123629-9116865c883b/go.mod h1:hD8YwSHusjwjEEgubW6XFvnZuNhMZTHz6lwjfltEt/Y=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
diff --git a/poller/poller.go b/poller/poller.go
index 98f1223..95f914a 100644
--- a/poller/poller.go
+++ b/poller/poller.go
@@ -9,6 +9,7 @@ import (
 
 	"gitea.com/gitea/act_runner/client"
 	"gitea.com/gitea/act_runner/config"
+	"gitea.com/gitea/act_runner/core"
 	runnerv1 "gitea.com/gitea/proto-go/runner/v1"
 
 	"github.com/appleboy/com/file"
@@ -44,16 +45,9 @@ type Poller struct {
 	errorRetryCounter int
 }
 
-type runner struct {
-	id    int64
-	uuid  string
-	name  string
-	token string
-}
-
 func (p *Poller) Register(ctx context.Context, cfg config.Runner) error {
 	// check .runner config exist
-	if file.IsFile(".runner") {
+	if file.IsFile(cfg.File) {
 		return nil
 	}
 
@@ -61,7 +55,6 @@ func (p *Poller) Register(ctx context.Context, cfg config.Runner) error {
 	resp, err := p.Client.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
 		Name:         cfg.Name,
 		Token:        cfg.Token,
-		Url:          cfg.URL,
 		AgentLabels:  append(defaultLabels, []string{p.Filter.OS, p.Filter.Arch}...),
 		CustomLabels: p.Filter.Labels,
 	}))
@@ -70,21 +63,21 @@ func (p *Poller) Register(ctx context.Context, cfg config.Runner) error {
 		return err
 	}
 
-	data := &runner{
-		id:    resp.Msg.Runner.Id,
-		uuid:  resp.Msg.Runner.Uuid,
-		name:  resp.Msg.Runner.Name,
-		token: resp.Msg.Runner.Token,
+	data := &core.Runner{
+		ID:    resp.Msg.Runner.Id,
+		UUID:  resp.Msg.Runner.Uuid,
+		Name:  resp.Msg.Runner.Name,
+		Token: resp.Msg.Runner.Token,
 	}
 
-	file, err := json.MarshalIndent(data, "", " ")
+	file, err := json.MarshalIndent(data, "", "  ")
 	if err != nil {
 		log.WithError(err).Error("poller: cannot marshal the json input")
 		return err
 	}
 
 	// store runner config in .runner file
-	return os.WriteFile(".runner", file, 0o644)
+	return os.WriteFile(cfg.File, file, 0o644)
 }
 
 func (p *Poller) Poll(ctx context.Context, n int) error {