diff --git a/cmd/joj3/main.go b/cmd/joj3/main.go
index b8e0e05..52da3fd 100644
--- a/cmd/joj3/main.go
+++ b/cmd/joj3/main.go
@@ -71,17 +71,26 @@ func outputResult(conf Conf, results []stage.StageResult) error {
 }
 
 func main() {
+	retCode := 0
+	defer func() {
+		os.Exit(retCode)
+	}()
 	conf, err := commitMsgToConf()
 	if err != nil {
 		slog.Error("no conf found", "error", err)
-		os.Exit(1)
+		retCode = 1
+		return
 	}
 	setupSlog(conf)
 	executors.InitWithConf(conf.SandboxExecServer, conf.SandboxToken)
 	stages := generateStages(conf)
 	defer stage.Cleanup()
-	results := stage.Run(stages)
+	results, stageErr := stage.Run(stages)
 	if err := outputResult(conf, results); err != nil {
 		slog.Error("output result", "error", err)
 	}
+	if stageErr != nil {
+		retCode = 1
+		return
+	}
 }
diff --git a/internal/stage/run.go b/internal/stage/run.go
index 670f666..46a0218 100644
--- a/internal/stage/run.go
+++ b/internal/stage/run.go
@@ -1,11 +1,11 @@
 package stage
 
 import (
+	"errors"
 	"log/slog"
 )
 
-func Run(stages []Stage) []StageResult {
-	stageResults := []StageResult{}
+func Run(stages []Stage) (stageResults []StageResult, stageErr error) {
 	for _, stage := range stages {
 		slog.Debug("stage start", "name", stage.Name)
 		slog.Debug("executor run start", "cmds", stage.ExecutorCmds)
@@ -38,10 +38,11 @@ func Run(stages []Stage) []StageResult {
 			ForceQuit: forceQuit,
 		})
 		if forceQuit {
+			stageErr = errors.New("parser force quit")
 			break
 		}
 	}
-	return stageResults
+	return
 }
 
 func Cleanup() {