Summary
The PostToolUse hook (semgrep mcp -k post-tool-cli-scan) does not honor nosemgrep: annotations, even when the annotation includes the exact rule ID and is placed on the same line as the violation. Stock semgrep CLI honors these annotations; the MCP variant does not.
This makes it impossible to cleanly suppress a rule for an identifier-only path resolver, a known-safe regex pattern, an audited deserialization site, etc., without editing code in ways that obscure intent.
Environment
- Plugin:
semgrep@claude-plugins-official v0.5.3
- Semgrep CLI: 1.159.0
- macOS 25.4.0 (Apple Silicon)
- Claude Code with the plugin enabled
Reproducer
A bounds-checked path resolver. The rule path-join-resolve-traversal fires on the resolve() call inside the validator. The nosemgrep annotation is added because the surrounding bounds-check guards the resolve:
import { resolve, relative as relativePath, sep } from 'path';
const PROJECT_ROOT = process.env.PROJECT_ROOT || resolve(process.env.HOME || '', 'project');
export const safePath = (input: string): string => {
if (typeof input !== 'string' || input.trim() === '') {
throw new Error('safePath: input must be a non-empty string');
}
if (input.includes('\0')) {
throw new Error('safePath: input contains null byte');
}
// Bounds-checked below; rejects inputs that escape PROJECT_ROOT.
const resolved = resolve(PROJECT_ROOT, input); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
const rel = relativePath(PROJECT_ROOT, resolved);
if (rel.startsWith('..') || rel.startsWith(sep)) {
throw new Error(`safePath: input escapes PROJECT_ROOT`);
}
if (rel === '' || rel === '.') {
throw new Error(`safePath: input resolves to PROJECT_ROOT itself`);
}
return resolved;
};
Hook output on edit:
PostToolUse:Edit hook blocking error from command: "semgrep mcp -k post-tool-cli-scan":
[{'line': N, 'message': 'Detected possible user input going into a `path.join` or
`path.resolve` function. ...', 'cwe': ["CWE-22"]}]
The nosemgrep annotation on the resolve line is ignored.
Forms tested
All ignored:
- Same line as the violation:
const x = resolve(ROOT, input); // nosemgrep: <rule-id>
- Line before, with the comment immediately preceding the violation
- Generic form without rule ID:
// nosemgrep
Stock semgrep --config=auto honors all three forms (https://semgrep.dev/docs/ignoring-files-folders-code).
Expected behavior
The MCP variant of semgrep should honor nosemgrep annotations using the same precedence rules as stock semgrep. If suppression is intentionally disabled in this mode, that should be documented and ideally configurable (a plugin setting, or a .semgrepignore-style file the plugin reads).
Why this matters
Developers implementing a sanitizer (path-bounds checker, regex-validator, parameterized SQL) routinely trigger pattern-matching rules at the implementation site of the validator itself. The rule's pattern can't statically reason about the validation. nosemgrep is the standard escape hatch. Without it, the only options are restructure code to avoid the pattern (often introduces real bugs) or live with a permanent "blocking" hook output on every edit to the file.
Related observations (possibly intentional design choices)
Filing here for visibility; happy to spin them off if useful:
-
Hook is advisory, not preventive. Despite the "blocking error" tag, the file write completes. The hook interrupts the agent but doesn't roll back. The "blocking error" wording misleads.
-
Whole-file scan on every edit. Editing one line of a large file produces findings for all pre-existing matches, not just newly-introduced ones. This is stock semgrep behavior, but the plugin could use --baseline-ref against the previous file state to surface only delta findings.
Suggested fix
If nosemgrep is intentionally disabled in post-tool-cli-scan: document this and offer a project-level allowlist mechanism.
If unintentional: align the hook's invocation with semgrep scan semantics so annotations work as users expect.
Summary
The
PostToolUsehook (semgrep mcp -k post-tool-cli-scan) does not honornosemgrep:annotations, even when the annotation includes the exact rule ID and is placed on the same line as the violation. StocksemgrepCLI honors these annotations; the MCP variant does not.This makes it impossible to cleanly suppress a rule for an identifier-only path resolver, a known-safe regex pattern, an audited deserialization site, etc., without editing code in ways that obscure intent.
Environment
semgrep@claude-plugins-officialv0.5.3Reproducer
A bounds-checked path resolver. The rule
path-join-resolve-traversalfires on theresolve()call inside the validator. Thenosemgrepannotation is added because the surrounding bounds-check guards the resolve:Hook output on edit:
The
nosemgrepannotation on the resolve line is ignored.Forms tested
All ignored:
const x = resolve(ROOT, input); // nosemgrep: <rule-id>// nosemgrepStock
semgrep --config=autohonors all three forms (https://semgrep.dev/docs/ignoring-files-folders-code).Expected behavior
The MCP variant of semgrep should honor
nosemgrepannotations using the same precedence rules as stock semgrep. If suppression is intentionally disabled in this mode, that should be documented and ideally configurable (a plugin setting, or a.semgrepignore-style file the plugin reads).Why this matters
Developers implementing a sanitizer (path-bounds checker, regex-validator, parameterized SQL) routinely trigger pattern-matching rules at the implementation site of the validator itself. The rule's pattern can't statically reason about the validation.
nosemgrepis the standard escape hatch. Without it, the only options are restructure code to avoid the pattern (often introduces real bugs) or live with a permanent "blocking" hook output on every edit to the file.Related observations (possibly intentional design choices)
Filing here for visibility; happy to spin them off if useful:
Hook is advisory, not preventive. Despite the "blocking error" tag, the file write completes. The hook interrupts the agent but doesn't roll back. The "blocking error" wording misleads.
Whole-file scan on every edit. Editing one line of a large file produces findings for all pre-existing matches, not just newly-introduced ones. This is stock semgrep behavior, but the plugin could use
--baseline-refagainst the previous file state to surface only delta findings.Suggested fix
If
nosemgrepis intentionally disabled inpost-tool-cli-scan: document this and offer a project-level allowlist mechanism.If unintentional: align the hook's invocation with
semgrep scansemantics so annotations work as users expect.