diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go
index f1ec5be5cbc..0f9e0913733 100644
--- a/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go
+++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go
@@ -48,3 +48,42 @@ func TestGenPbIssue3882(t *testing.T) {
 		t.Assert(gstr.Contains(genContent, exceptText), true)
 	})
 }
+
+// This issue only occurs when executing multiple times
+// and the subsequent OutputApi is the parent directory of the previous execution
+func TestGenPbIssue3953(t *testing.T) {
+	gtest.C(t, func(t *gtest.T) {
+		var (
+			outputPath     = gfile.Temp(guid.S())
+			outputApiPath  = filepath.Join(outputPath, "api")
+			outputCtrlPath = filepath.Join(outputPath, "controller")
+
+			protobufFolder = gtest.DataPath("issue", "3953")
+			in             = genpb.CGenPbInput{
+				Path:       protobufFolder,
+				OutputApi:  outputApiPath,
+				OutputCtrl: outputCtrlPath,
+			}
+			err error
+		)
+		err = gfile.Mkdir(outputApiPath)
+		t.AssertNil(err)
+		err = gfile.Mkdir(outputCtrlPath)
+		t.AssertNil(err)
+		defer gfile.Remove(outputPath)
+
+		_, err = genpb.CGenPb{}.Pb(ctx, in)
+		// do twice,and set outputApi to outputPath
+		in.OutputApi = outputPath
+		_, err = genpb.CGenPb{}.Pb(ctx, in)
+		t.AssertNil(err)
+
+		var (
+			genContent = gfile.GetContents(filepath.Join(outputApiPath, "issue3953.pb.go"))
+			// The old version would have appeared `v:"required" v:"required"`
+			// but the new version of the code will appear `v:"required"` only once
+			notExceptText = `v:"required" v:"required"`
+		)
+		t.Assert(gstr.Contains(genContent, notExceptText), false)
+	})
+}
diff --git a/cmd/gf/internal/cmd/genpb/genpb_tag.go b/cmd/gf/internal/cmd/genpb/genpb_tag.go
index f8cfad7e5b8..ac74e28ebce 100644
--- a/cmd/gf/internal/cmd/genpb/genpb_tag.go
+++ b/cmd/gf/internal/cmd/genpb/genpb_tag.go
@@ -71,6 +71,10 @@ func (c CGenPb) doTagReplacement(ctx context.Context, content string) (string, e
 			if !lineTagMap.IsEmpty() {
 				tagContent := c.listMapToStructTag(lineTagMap)
 				lineTagMap.Clear()
+				// If already have it, don't add it anymore
+				if gstr.Contains(gstr.StrTill(line, "` //"), tagContent) {
+					continue
+				}
 				line, _ = gregex.ReplaceString("`(.+)`", fmt.Sprintf("`$1 %s`", tagContent), line)
 			}
 			lines[index] = line
diff --git a/cmd/gf/internal/cmd/testdata/issue/3953/issue3953.proto b/cmd/gf/internal/cmd/testdata/issue/3953/issue3953.proto
new file mode 100644
index 00000000000..8bc86fba1f7
--- /dev/null
+++ b/cmd/gf/internal/cmd/testdata/issue/3953/issue3953.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+package account;
+
+option go_package = "account/v1";
+
+service Account {
+  rpc getUserByIds (Req) returns (Resp) {
+  }
+}
+
+message Req {
+  repeated int64 ids = 1; // v: required
+}
+message Resp {
+  repeated string data = 1;
+}