diff --git a/.gitmodules b/.gitmodules
index 71c5721..d6f32b0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -30,3 +30,7 @@
 	path = examples/keyword/clangtidy/sillycode
 	url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git
 	branch = keyword/clangtidy/sillycode
+[submodule "examples/cppcheck/sillycode"]
+	path = examples/cppcheck/sillycode
+	url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git
+	branch = cppcheck/sillycode
diff --git a/examples/cppcheck/sillycode b/examples/cppcheck/sillycode
new file mode 160000
index 0000000..0815ab9
--- /dev/null
+++ b/examples/cppcheck/sillycode
@@ -0,0 +1 @@
+Subproject commit 0815ab90d72641fc274231c075282567e9e17865
diff --git a/internal/parsers/all.go b/internal/parsers/all.go
index 4decc13..615f90c 100644
--- a/internal/parsers/all.go
+++ b/internal/parsers/all.go
@@ -2,6 +2,7 @@ package parsers
 
 import (
 	_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/clangtidy"
+	_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/cppcheck"
 	_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/cpplint"
 	_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/diff"
 	_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/dummy"
diff --git a/internal/parsers/cppcheck/meta.go b/internal/parsers/cppcheck/meta.go
new file mode 100644
index 0000000..b811ad4
--- /dev/null
+++ b/internal/parsers/cppcheck/meta.go
@@ -0,0 +1,9 @@
+package cppcheck
+
+import "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
+
+var name = "cppcheck"
+
+func init() {
+	stage.RegisterParser(name, &CppCheck{})
+}
diff --git a/internal/parsers/cppcheck/parser.go b/internal/parsers/cppcheck/parser.go
new file mode 100644
index 0000000..69f39cc
--- /dev/null
+++ b/internal/parsers/cppcheck/parser.go
@@ -0,0 +1,85 @@
+package cppcheck
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
+	"github.com/criyle/go-judge/envexec"
+)
+
+type CppCheck struct{}
+
+type Match struct {
+	Severity []string
+	Score    int
+}
+
+type Conf struct {
+	Score   int `default:"100"`
+	Matches []Match
+}
+
+type Record struct {
+	File     string `json:"file"`
+	Line     int    `json:"line"`
+	Column   int    `json:"column"`
+	Severity string `json:"severity"`
+	Message  string `json:"message"`
+	Id       string `json:"id"`
+}
+
+func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
+	// stdout := executorResult.Files["stdout"]
+	stderr := executorResult.Files["stderr"]
+
+	if executorResult.Status != stage.Status(envexec.StatusAccepted) {
+		return stage.ParserResult{
+			Score: 0,
+			Comment: fmt.Sprintf(
+				"Unexpected executor status: %s.\nStderr: %s",
+				executorResult.Status, stderr,
+			),
+		}
+	}
+	records := make([]Record, 0)
+	lines := strings.Split(stderr, "\n")
+	for _, line := range lines {
+		if strings.TrimSpace(line) == "" {
+			continue
+		}
+		var record Record
+		_ = json.Unmarshal([]byte(line), &record)
+		records = append(records, record)
+	}
+	comment, score, err := GetResult(records, conf)
+	if err != nil {
+		return stage.ParserResult{
+			Score: 0,
+			Comment: fmt.Sprintf(
+				"Unexpected parser error: %s.",
+				err,
+			),
+		}
+	}
+
+	return stage.ParserResult{
+		Score:   score,
+		Comment: comment,
+	}
+}
+
+func (*CppCheck) Run(results []stage.ExecutorResult, confAny any) (
+	[]stage.ParserResult, bool, error,
+) {
+	conf, err := stage.DecodeConf[Conf](confAny)
+	if err != nil {
+		return nil, true, err
+	}
+	var res []stage.ParserResult
+	for _, result := range results {
+		res = append(res, Parse(result, *conf))
+	}
+	return res, false, nil
+}
diff --git a/internal/parsers/cppcheck/score.go b/internal/parsers/cppcheck/score.go
new file mode 100644
index 0000000..d6028fb
--- /dev/null
+++ b/internal/parsers/cppcheck/score.go
@@ -0,0 +1,66 @@
+package cppcheck
+
+import "fmt"
+
+type Severity int
+
+const (
+	ERROR Severity = iota
+	WARNING
+	PROBABILITY
+	PERFORMANCE
+	STYLE
+	INFORMATION
+	UNKNOWN
+)
+
+func severityFromString(severityString string) (Severity, error) {
+	switch severityString {
+	case "error":
+		return ERROR, nil
+	case "warning":
+		return WARNING, nil
+	case "probability":
+		return PROBABILITY, nil
+	case "performance":
+		return PERFORMANCE, nil
+	case "style":
+		return STYLE, nil
+	case "information":
+		return INFORMATION, nil
+	default:
+		return UNKNOWN, fmt.Errorf("unkown severity type \"%s\" for cppcheck", severityString)
+	}
+}
+
+func GetResult(records []Record, conf Conf) (string, int, error) {
+	result := "### Test results summary\n\n"
+	var severityCounts [6]int
+	var severityScore [6]int
+	score := conf.Score
+
+	for _, match := range conf.Matches {
+		severities := match.Severity
+		score := match.Score
+		for _, severityString := range severities {
+			severity, err := severityFromString(severityString)
+			if err != nil {
+				return "", 0, err
+			}
+			severityScore[int(severity)] = score
+		}
+	}
+
+	for _, record := range records {
+		severity, _ := severityFromString(record.Severity)
+		severityCounts[int(severity)] += 1
+		score -= severityScore[int(severity)]
+	}
+	result += fmt.Sprintf("1. error: %d\n", severityCounts[0])
+	result += fmt.Sprintf("2. warning: %d\n", severityCounts[1])
+	result += fmt.Sprintf("3. probability: %d\n", severityCounts[2])
+	result += fmt.Sprintf("4. performance: %d\n", severityCounts[3])
+	result += fmt.Sprintf("5. style: %d\n", severityCounts[4])
+	result += fmt.Sprintf("6. information: %d", severityCounts[5])
+	return result, score, nil
+}