A Vibe Coding Tools plugin that detects semantic trap words in Skill / Prompt / Agent instruction files — words with overly broad semantic boundaries that can cause LLM hallucinations.
Using "risk" instead of "vulnerability" in the same Skill can cause a 27% accuracy drop, even with identical constraints and logic.
This project evolved from the theoretical framework presented in Don't Let LLMs "Overthink": Semantic Traps and Anti-Hallucination Design in SKILL Development, turning its semantic trap detection methodology into an automated tool.
In LLM instruction files, some word pairs appear synonymous to humans but activate vastly different semantic regions in the model:
| Wide (Risky) | Narrow (Precise) | Why It Matters |
|---|---|---|
| risk | vulnerability | "risk" triggers financial, health, legal associations — model loses focus |
| review | check | "review" implies subjective evaluation — model hallucinates opinions |
| issue | defect | "issue" activates debate/controversy meanings — model drifts off-topic |
| analyze | summarize | "analyze" has no boundary — model produces unbounded output |
semantic-linter catches these traps automatically every time you edit an instruction file.
- 27 semantic trap pairs: 17 Chinese + 10 English, each with severity rating and replacement suggestion
- Context-aware severity: The same word gets different risk levels depending on its role (constraint keyword > task target > auxiliary)
- 4 structural risk detectors: Open-ended verbs, abstract targets, modal downgrades, missing negation lists
- Code block exclusion: Skips
```fenced blocks and`inline code to avoid false positives - Bilingual: Full Chinese and English support with language-specific detection strategies
- Zero dependencies: Pure Node.js, no npm install needed
- Non-blocking: Never interrupts Claude's workflow — findings are injected as warnings
Vercel Skills CLI is a good fit when you want a reusable skill that is not tied to a specific AI tool. If you only want to try the lightweight single-file reference skill from this repository, use:
npx skills add SummerSec/semantic-linter --skill semantic-linter-shotIf the current session does not pick up the new skill immediately, restart your AI tool.
If you want the full plugin experience in Claude Code, first add this repository as a plugin marketplace:
claude plugin marketplace add SummerSec/semantic-linterThen install the plugin itself:
claude plugin install semantic-linter@summersec-semantic-linterIf the current session does not pick up the plugin immediately after installation, run:
/reload-pluginsWhen updating, refresh the marketplace cache first and then update the plugin:
# Refresh marketplace cache first, then update
claude plugin marketplace update summersec-semantic-linter
claude plugin update semantic-linter@summersec-semantic-linterIf you prefer to work directly from source, or want a local development install, clone the repository into your Claude plugins directory:
git clone https://github.com/SummerSec/semantic-linter.git ~/.claude/plugins/semantic-linterThen manually register it in ~/.claude/plugins/installed_plugins.json:
{
"version": 2,
"plugins": {
"semantic-linter@summersec-semantic-linter": [
{
"scope": "user",
"installPath": "/Users/<you>/.claude/plugins/semantic-linter",
"version": "1.1.0"
}
]
}
}On Windows, use C:/Users/<you>/.claude/plugins/semantic-linter as installPath.
After registration, restart Claude Code, or run /reload-plugins. To update a source install:
cd ~/.claude/plugins/semantic-linter
git pullThe linter activates on files matching these patterns:
| Pattern | Examples |
|---|---|
| File names | skill.md, SKILL.md, claude.md, CLAUDE.md |
| Suffixes | *.prompt.md, *_definitions.md, *_examples.md |
| Directories | /skills/, /agents/, /commands/, /rules/, /prompts/ |
All other files are silently skipped.
Place .semantic-linter.json in the repo or a parent directory. The linter walks upward from the scanned file’s directory and uses the nearest config file.
| Field | Purpose |
|---|---|
ignoreTrapIds |
Array of trap IDs to suppress (e.g. ["T01"]) |
ignorePathSubstrings |
If the normalized path contains a substring, the entire file is skipped |
ignoreStructuralTypes |
e.g. ["open_ended_verb"] to turn off specific structural rules |
UserPromptSubmit scans use the same config, discovered from the current working directory upward.
Hooks and the CLI persist stats under the user home directory (override with SEMANTIC_LINTER_STATE_DIR):
- Default:
$HOME/.semantic-linter/(Windows:%USERPROFILE%\.semantic-linter\) - Files:
stats.json,session.json— may include paths of files that were scanned - Detection counts are updated on PostToolUse (and user-prompt scans), not on PreToolUse, so the same edit is not double-counted.
Authoritative table: plugin/references/semantic-trap-lexicon.md. After editing it:
npm run build-lexicon
npm testValidate committed output without writing: npm run build-lexicon:check
--json includes schemaVersion, version (from root package.json), per-file skipped (path ignored by config), and summary.filesSkipped.
File Detection ──→ Content Scanning ──→ Structural Analysis ──→ Report
(path match) (lexicon match) (pattern detection) (markdown)
Checks file path against known instruction-file patterns. Only .md files are considered.
- Strips code blocks to prevent false positives
- Matches text against 27 trap word pairs (O(1) Map lookup)
- Classifies each match's context role:
- constraint_keyword — highest risk (e.g. phrases with 只/必须/不得 + wide words)
- task_target — medium risk (e.g. "please analyze …")
- auxiliary — lower risk; a bare 不 is not treated as a constraint marker (reduces false positives)
- Deduplicates: each word reported only once per file
Detects four structural risk patterns:
| Risk Type | Example (Flagged) | Example (OK) |
|---|---|---|
| Open-ended verb | "Analyze the code" | "Analyze the code for the following aspects" |
| Abstract target | "Evaluate security" | "Detect vulnerabilities" |
| Modal downgrade | "should not" in constraints | "must not" in constraints |
| Missing negation | High-severity word, no exclusion list | Word + "excluding..." |
Generates a structured Markdown report with:
- Overall risk level
- Trap words table (word, ID, severity, context, replacement, line number)
- Structural risks (type, context, suggestion)
- Action recommendations
critical > high > medium-high > medium > low
Severity is adjusted by context role:
- Constraint keyword: base severity (no change)
- Task target: base severity (no change)
- Auxiliary: downgraded by 1 level
## Semantic Trap Detection Report
**File**: skills/code-review/skill.md
**Findings**: 2 trap words, 1 structural risk
**Overall Risk**: HIGH
### Trap Words
| # | Word | ID | Severity | Context | Replace With | Line |
|---|------|-----|----------|----------------|-------------|------|
| 1 | risk | E01 | critical | constraint_keyword | vulnerability | 12 |
| 2 | review | E02 | high | task_target | check | 5 |
### Structural Risks
| Type | Scope | Context | Suggestion |
|------|-------|---------|------------|
| Open-ended verb | Line 8 | "Analyze the code" | Add scope limiters |npm test33 test cases covering all 4 modules, using Node.js built-in assert (zero test framework dependencies).
semantic-linter/
├── hooks/
│ ├── hooks.json # Hook registration config
│ └── semantic-linter.js # Hook entry point
├── lib/
│ ├── file-detector.js # Stage 1: Path pattern matching
│ ├── content-scanner.js # Stage 2: Lexicon matching + context
│ ├── lexicon-data.js # Trap word database (27 pairs)
│ ├── structural-analyzer.js # Stage 3: Structural risk detection
│ └── report-formatter.js # Stage 4: Markdown report generation
├── tests/
│ └── test-scanner.js # 33 test cases
├── references/
│ └── semantic-trap-lexicon.md # Full lexicon documentation
├── package.json
└── CLAUDE.md
- O(1) lexicon lookup — Map-based, not linear search
- Context-aware severity — Same word, different risk depending on sentence role
- Code block stripping — No false positives from documentation examples
- Deduplication — Each trap word reported once per file (
pairId:wordkey) - Graceful failure — Hook always returns
continue: true, never blocks Claude - Separate language paths — Chinese uses substring matching; English uses word-boundary regex
MIT