diff --git a/cmd/joj3/conf.go b/cmd/joj3/conf.go
index ee48f51..d546c0c 100644
--- a/cmd/joj3/conf.go
+++ b/cmd/joj3/conf.go
@@ -8,7 +8,6 @@ import (
 	"regexp"
 
 	"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
-	"github.com/go-git/go-git/v5"
 	"github.com/koding/multiconfig"
 )
 
@@ -122,25 +121,11 @@ func parseConfFile(path string) (conf Conf, err error) {
 	return
 }
 
-func commitMsgToConf(metaConfPath string) (conf Conf, err error) {
+func msgToConf(metaConfPath string, msg string) (conf Conf, err error) {
 	metaConf, err := parseMetaConfFile(metaConfPath)
 	if err != nil {
 		return
 	}
-	r, err := git.PlainOpen(".")
-	if err != nil {
-		return
-	}
-	ref, err := r.Head()
-	if err != nil {
-		return
-	}
-	commit, err := r.CommitObject(ref.Hash())
-	if err != nil {
-		return
-	}
-	msg := commit.Message
-	slog.Debug("commit msg to conf", "msg", msg)
 	for _, pattern := range metaConf.Patterns {
 		if matched, _ := regexp.MatchString(pattern.Regex, msg); matched {
 			slog.Debug("pattern matched", "pattern", pattern)
diff --git a/cmd/joj3/main.go b/cmd/joj3/main.go
index 8b05c76..65e03bd 100644
--- a/cmd/joj3/main.go
+++ b/cmd/joj3/main.go
@@ -10,6 +10,7 @@ import (
 	_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers"
 	"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
 
+	"github.com/go-git/go-git/v5"
 	"github.com/jinzhu/copier"
 )
 
@@ -22,6 +23,23 @@ func setupSlog(logLevel int) {
 	slog.SetDefault(logger)
 }
 
+func getCommitMsg() (msg string, err error) {
+	r, err := git.PlainOpen(".")
+	if err != nil {
+		return
+	}
+	ref, err := r.Head()
+	if err != nil {
+		return
+	}
+	commit, err := r.CommitObject(ref.Hash())
+	if err != nil {
+		return
+	}
+	msg = commit.Message
+	return
+}
+
 func generateStages(conf Conf) ([]stage.Stage, error) {
 	stages := []stage.Stage{}
 	for _, s := range conf.Stages {
@@ -71,20 +89,33 @@ func outputResult(outputPath string, results []stage.StageResult) error {
 		append(content, []byte("\n")...), 0o600)
 }
 
-var metaConfPath string
+var (
+	metaConfPath string
+	msg          string
+)
 
 func init() {
 	flag.StringVar(&metaConfPath, "meta-conf", "meta-conf.json", "meta config file path")
+	flag.StringVar(&msg, "msg", "", "message to trigger the running, leave empty to use git commit message on HEAD")
 }
 
 func mainImpl() error {
+	setupSlog(int(slog.LevelInfo)) // before conf is loaded
 	flag.Parse()
-	conf, err := commitMsgToConf(metaConfPath)
+	if msg == "" {
+		var err error
+		msg, err = getCommitMsg()
+		if err != nil {
+			slog.Error("get commit msg", "error", err)
+			return err
+		}
+	}
+	conf, err := msgToConf(metaConfPath, msg)
 	if err != nil {
 		slog.Error("no conf found", "error", err)
 		return err
 	}
-	setupSlog(conf.LogLevel)
+	setupSlog(conf.LogLevel) // after conf is loaded
 	executors.InitWithConf(conf.SandboxExecServer, conf.SandboxToken)
 	stages, err := generateStages(conf)
 	if err != nil {