ctxsentry 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctxsentry-0.4.0/.gitignore +27 -0
- ctxsentry-0.4.0/CHANGELOG.md +100 -0
- ctxsentry-0.4.0/LICENSE +21 -0
- ctxsentry-0.4.0/PKG-INFO +256 -0
- ctxsentry-0.4.0/README.md +221 -0
- ctxsentry-0.4.0/benchmark/benign/api_docs.md +11 -0
- ctxsentry-0.4.0/benchmark/benign/architecture.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/changelog.md +7 -0
- ctxsentry-0.4.0/benchmark/benign/clean_mcp/.mcp.json +22 -0
- ctxsentry-0.4.0/benchmark/benign/code_of_conduct.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/contributing.md +8 -0
- ctxsentry-0.4.0/benchmark/benign/curl_download.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/env_setup.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/faq.md +7 -0
- ctxsentry-0.4.0/benchmark/benign/install_guide.md +11 -0
- ctxsentry-0.4.0/benchmark/benign/makefile_notes.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/normal_readme.md +17 -0
- ctxsentry-0.4.0/benchmark/benign/prompt_injection_explainer.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/prompt_notes.md +3 -0
- ctxsentry-0.4.0/benchmark/benign/release_process.md +6 -0
- ctxsentry-0.4.0/benchmark/benign/security_policy.md +7 -0
- ctxsentry-0.4.0/benchmark/benign/system_requirements.md +9 -0
- ctxsentry-0.4.0/benchmark/benign/troubleshooting.md +5 -0
- ctxsentry-0.4.0/benchmark/cases.jsonl +38 -0
- ctxsentry-0.4.0/benchmark/malicious/base64_payload.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/bidi_trojan_source.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/claude_md_standing_exfil.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/dns_exfil.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/fake_context_boundary.md +7 -0
- ctxsentry-0.4.0/benchmark/malicious/git_exfil.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/hex_payload.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/hidden_style_span.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/indirect_via_issue_quote.md +5 -0
- ctxsentry-0.4.0/benchmark/malicious/jailbreak.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/pipe_shell_mcp/mcp.json +11 -0
- ctxsentry-0.4.0/benchmark/malicious/poisoned_mcp/.mcp.json +17 -0
- ctxsentry-0.4.0/benchmark/malicious/readme_html_comment.md +9 -0
- ctxsentry-0.4.0/benchmark/malicious/readme_plain_exfil.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/reverse_shell.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/role_delimiter_tokens.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/split_payload.md +5 -0
- ctxsentry-0.4.0/benchmark/malicious/system_prompt_leak.md +3 -0
- ctxsentry-0.4.0/benchmark/malicious/unicode_tag_smuggle.md +7 -0
- ctxsentry-0.4.0/benchmark/malicious/zerowidth_spaced_payload.md +3 -0
- ctxsentry-0.4.0/docs/RELEASING.md +45 -0
- ctxsentry-0.4.0/docs/threat-model.md +74 -0
- ctxsentry-0.4.0/docs/walkthrough.md +137 -0
- ctxsentry-0.4.0/pyproject.toml +64 -0
- ctxsentry-0.4.0/src/ctxsentry/__init__.py +13 -0
- ctxsentry-0.4.0/src/ctxsentry/__main__.py +6 -0
- ctxsentry-0.4.0/src/ctxsentry/benchmark.py +121 -0
- ctxsentry-0.4.0/src/ctxsentry/cli.py +249 -0
- ctxsentry-0.4.0/src/ctxsentry/contexts.py +164 -0
- ctxsentry-0.4.0/src/ctxsentry/detectors.py +626 -0
- ctxsentry-0.4.0/src/ctxsentry/document.py +53 -0
- ctxsentry-0.4.0/src/ctxsentry/finding.py +74 -0
- ctxsentry-0.4.0/src/ctxsentry/report.py +169 -0
- ctxsentry-0.4.0/src/ctxsentry/rules.py +335 -0
- ctxsentry-0.4.0/src/ctxsentry/scanner.py +322 -0
- ctxsentry-0.4.0/tests/conftest.py +4 -0
- ctxsentry-0.4.0/tests/test_benchmark.py +22 -0
- ctxsentry-0.4.0/tests/test_cli_and_report.py +116 -0
- ctxsentry-0.4.0/tests/test_detectors.py +241 -0
- ctxsentry-0.4.0/tests/test_packaging.py +30 -0
- ctxsentry-0.4.0/tests/test_scanner.py +153 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Tooling
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
coverage.xml
|
|
19
|
+
|
|
20
|
+
# ctxsentry output
|
|
21
|
+
ctxsentry.json
|
|
22
|
+
ctxsentry.sarif
|
|
23
|
+
|
|
24
|
+
# Editors / OS
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
.DS_Store
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
> Releases before 0.4.0 were published under the name **ctxguard**; that name was
|
|
7
|
+
> already taken on PyPI. Everything below is written with the current name.
|
|
8
|
+
|
|
9
|
+
## [0.4.0] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Renamed the project `ctxguard` → `ctxsentry`.** The PyPI distribution, the
|
|
13
|
+
CLI command, the import package, the ignore file (`.ctxsentryignore`) and the
|
|
14
|
+
inline suppression marker (`ctxsentry: ignore`) all use the new name. The
|
|
15
|
+
GitHub repository moved to `EivinDDu/ctxsentry` (old links redirect).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Published to PyPI** — `pip install ctxsentry` / `pipx install ctxsentry`.
|
|
19
|
+
- `.github/workflows/publish.yml` — tagged releases build an sdist + wheel and
|
|
20
|
+
publish to PyPI via OIDC Trusted Publishing (no stored token).
|
|
21
|
+
- `docs/RELEASING.md` — the release checklist and one-time PyPI setup.
|
|
22
|
+
- Richer package metadata: per-version Python classifiers, `Repository` /
|
|
23
|
+
`Changelog` project URLs, and an sdist that ships the tests and benchmark.
|
|
24
|
+
|
|
25
|
+
## [0.3.0] - 2026-09-06
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **Benchmark harness** — `ctxsentry bench` scores the detectors against a
|
|
29
|
+
labelled corpus in `benchmark/` (38 cases) and reports precision, recall, F1,
|
|
30
|
+
false-positive rate and per-rule accuracy. `--min-recall` / `--max-fp-rate` /
|
|
31
|
+
`--min-rule-accuracy` make it a CI gate; CI now fails on any detection
|
|
32
|
+
regression or new false positive.
|
|
33
|
+
- **`CG406` — deobfuscated rescan.** Zero-width / Unicode-Tag characters are
|
|
34
|
+
stripped and common Cyrillic/Greek homoglyphs folded to ASCII, then the rule
|
|
35
|
+
table is re-run. Catches `Ignore all previous instructions` and
|
|
36
|
+
`іgnоrе …`-style evasions that defeat a raw regex.
|
|
37
|
+
- **`CG107`** — attempts to make the model disclose its own prompt / prior
|
|
38
|
+
context (`repeat the text above verbatim`, `what is your system prompt`).
|
|
39
|
+
- **`CG203`** — fake context boundaries and `system override` banners used to
|
|
40
|
+
inject a task into retrieved / tool-result content.
|
|
41
|
+
- **`CG307`** — DNS-based exfiltration (`dig $(cat …).attacker`).
|
|
42
|
+
- **`CG308`** — exfiltration / persistence via `git remote add` + `git push` to
|
|
43
|
+
a non-GitHub URL, or a `postinstall` hook that shells out.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- `CG302` callback-URL list extended with tunnel / relay services
|
|
47
|
+
(`trycloudflare.com`, `loca.lt`, `localtunnel.me`, `serveo.net`, `lhr.life`,
|
|
48
|
+
`smee.io`, `hookb.in`, `webhookrelay.com`, `dnslog.cn`, `ngrok-free.app`).
|
|
49
|
+
- `CG103` no longer fires on ordinary `### System Requirements`-style headings
|
|
50
|
+
(needs a bare role header, a trailing colon, or `### System prompt`).
|
|
51
|
+
- `CG802` (instruction-like filename) now needs a real imperative phrase, not
|
|
52
|
+
just a keyword like `curl` — `curl_download.md` is no longer flagged.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- Two false positives found by the new benchmark corpus (the `CG103` and
|
|
56
|
+
`CG802` cases above).
|
|
57
|
+
|
|
58
|
+
## [0.2.0] - 2026-09-07
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
- `--changed [REF]` — scan only files changed vs `REF` (default `HEAD`) plus
|
|
62
|
+
staged / unstaged / untracked files. Fast pre-commit and PR-diff gating.
|
|
63
|
+
- Encoded-payload detector: base64 and hex blobs are decoded and the plaintext
|
|
64
|
+
rescanned (`CG404`, with the decoded text in the finding). `CG403` is now the
|
|
65
|
+
low-severity "long blob that doesn't decode to text" note.
|
|
66
|
+
- `CG106` — jailbreak / guardrail-removal phrasing (`developer mode`,
|
|
67
|
+
`do anything now`, `ignore your guidelines`, …).
|
|
68
|
+
- `CG306` — reverse-shell command patterns (`bash -i >& /dev/tcp/…`, `nc -e`,
|
|
69
|
+
`python -c '…socket…'`).
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- `ctxsentry rules` now lists the analytic detectors (`CG4xx`–`CG8xx`) alongside
|
|
73
|
+
the regex rules.
|
|
74
|
+
- `CG403` moved from the regex table into the encoded-payload detector.
|
|
75
|
+
|
|
76
|
+
## [0.1.0] - 2026-09-05
|
|
77
|
+
|
|
78
|
+
Initial release.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- `ctxsentry scan <path>` — static scan of a repository for inbound
|
|
82
|
+
prompt-injection payloads aimed at AI coding agents.
|
|
83
|
+
- Context classifier (`mcp-config`, `agent-instructions`, `agent-skill`,
|
|
84
|
+
`docs`, `generic`) with per-context severity boosting.
|
|
85
|
+
- Detectors: regex rule table (instruction override, agent-directed
|
|
86
|
+
imperatives, exfiltration, obfuscation), invisible-Unicode scanner with
|
|
87
|
+
`U+E00xx` tag-run decoding, bidi / zero-width / PUA / homoglyph detection,
|
|
88
|
+
layout smuggling, MCP `.mcp.json` structure walk, filename checks.
|
|
89
|
+
- Output formats: `text`, `json`, `sarif` (2.1.0), `markdown`.
|
|
90
|
+
- `--fail-on`, `--min-severity`, `--min-confidence`, `--all-text`,
|
|
91
|
+
`--git-history`, `--exclude`, `-o/--output`.
|
|
92
|
+
- Suppression via `.ctxsentryignore` and inline `ctxsentry: ignore [RULE…]`
|
|
93
|
+
comments.
|
|
94
|
+
- `ctxsentry rules` — list all detection rules.
|
|
95
|
+
- CI matrix (Python 3.9–3.13, Linux + macOS); dogfood self-scan and SARIF
|
|
96
|
+
artifact build on every run.
|
|
97
|
+
- `.pre-commit-hooks.yaml` — usable as a hosted pre-commit repo
|
|
98
|
+
(`repo: https://github.com/EivinDDu/ctxsentry`, `rev: v0.1.0`).
|
|
99
|
+
- `action.yml` — composite GitHub Action (`uses: EivinDDu/ctxsentry@v0.1.0`)
|
|
100
|
+
with `path`, `fail-on`, `args`, and `version` inputs.
|
ctxsentry-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EivinDDu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ctxsentry-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ctxsentry
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Scan a repository for prompt-injection payloads before you point an AI coding agent at it.
|
|
5
|
+
Project-URL: Homepage, https://github.com/EivinDDu/ctxsentry
|
|
6
|
+
Project-URL: Repository, https://github.com/EivinDDu/ctxsentry
|
|
7
|
+
Project-URL: Issues, https://github.com/EivinDDu/ctxsentry/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/EivinDDu/ctxsentry/blob/main/CHANGELOG.md
|
|
9
|
+
Author: EivinDDu
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-security,coding-agent,devsecops,llm-security,mcp,prompt-injection,security-scanner,static-analysis,supply-chain
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Security
|
|
27
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# ctxsentry
|
|
37
|
+
|
|
38
|
+
**Scan a repository for prompt-injection payloads *before* you point an AI coding agent at it.**
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/ctxsentry/)
|
|
41
|
+
[](https://github.com/EivinDDu/ctxsentry/actions/workflows/ci.yml)
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
<sub>Formerly published as `ctxguard` (≤ 0.3.0); renamed because that name was taken on PyPI.</sub>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## The gap this fills
|
|
50
|
+
|
|
51
|
+
Every current MCP / AI-agent security scanner points *outward* — it probes a
|
|
52
|
+
remote MCP server, or audits the agent framework you are building. Almost nothing
|
|
53
|
+
points *inward* at the **untrusted repository you are about to open in Cursor,
|
|
54
|
+
Claude Code, Copilot, or Windsurf**.
|
|
55
|
+
|
|
56
|
+
That repo is attacker-influenced text. The moment your agent reads its
|
|
57
|
+
`README.md`, `CLAUDE.md`, `.cursorrules`, `.mcp.json`, issue templates, or even a
|
|
58
|
+
filename, any instructions hidden in that content enter the model's context.
|
|
59
|
+
Cloud Security Alliance research in 2026 documented "README instruction
|
|
60
|
+
injection" against coding agents with attack success rates of 41–84%, and MCP
|
|
61
|
+
tool-description poisoning (CVE-2025-54136) works the same way.
|
|
62
|
+
|
|
63
|
+
`ctxsentry` is a fast, dependency-free static scanner that flags that content so a
|
|
64
|
+
human reviews it first.
|
|
65
|
+
|
|
66
|
+
**→ See [`docs/walkthrough.md`](docs/walkthrough.md) for a worked example: five
|
|
67
|
+
planted attacks in a sample repo, and the scan that catches every one.**
|
|
68
|
+
|
|
69
|
+
## What it detects
|
|
70
|
+
|
|
71
|
+
| Family | Examples |
|
|
72
|
+
|---|---|
|
|
73
|
+
| **Instruction override** | `ignore all previous instructions`, `you are now…`, chat-template tokens (`<system>`, `[INST]`, `<\|im_start\|>`), `do not tell the user`, `<IMPORTANT>` priority markers, jailbreak / guardrail-removal phrasing (`developer mode`, `do anything now`, `ignore your guidelines`) |
|
|
74
|
+
| **Agent-directed imperatives** | sentences addressed to "the AI / assistant / agent" that also name an action (`run`, `curl`, `exfiltrate`, `install`, `push`) |
|
|
75
|
+
| **Data exfiltration & RCE** | instructions to send `.env` / tokens / file contents somewhere, callback URLs (`webhook.site`, `ngrok`, `oast`, `requestbin`…), markdown images with query strings, `curl … \| sh`, reverse-shell one-liners (`bash -i >& /dev/tcp/…`) |
|
|
76
|
+
| **Data exfiltration & RCE** *(cont.)* | DNS exfiltration (`dig $(cat …).attacker`), `git remote add` + `git push` to a non-GitHub URL, `postinstall` hooks that shell out |
|
|
77
|
+
| **Context / prompt disclosure** | `repeat the text above verbatim`, `what is your system prompt`, fake `--- END OF DOCUMENT ---` / `system override:` boundaries injected into retrieved content |
|
|
78
|
+
| **Hidden Unicode** | Unicode **Tag** characters `U+E00xx` (decoded and shown), bidirectional overrides (Trojan Source), zero-width runs, Private-Use-Area smuggling, Latin/Cyrillic/Greek homoglyph words |
|
|
79
|
+
| **Deobfuscated rescan** | strips zero-width / tag characters and folds homoglyphs, then re-runs every rule — catches `Ignore all previous instructions` and `іgnоrе …` evasions |
|
|
80
|
+
| **Encoded payloads** | base64 / hex blobs are decoded and the plaintext rescanned — a hidden `ignore all previous instructions…` inside a base64 string is surfaced with the decoded text |
|
|
81
|
+
| **Layout smuggling** | instruction text pushed off-screen by whitespace, `display:none` / `color:#fff` / `font-size:0` spans, instruction-bearing HTML comments |
|
|
82
|
+
| **MCP config poisoning** | `.mcp.json` `description` / `instructions` fields carrying hidden directives or secret references; server launch commands that pipe a download into a shell |
|
|
83
|
+
| **Filename injection** | control / invisible / bidi characters in filenames, filenames that spell out an imperative |
|
|
84
|
+
|
|
85
|
+
Severity is **boosted by context**: the same string is `medium` in a source
|
|
86
|
+
comment but `critical` in `.mcp.json` or `CLAUDE.md`, because agents load those
|
|
87
|
+
files as authoritative instructions.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pipx install ctxsentry # recommended
|
|
93
|
+
# or
|
|
94
|
+
pip install ctxsentry
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
From source:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/EivinDDu/ctxsentry
|
|
101
|
+
cd ctxsentry
|
|
102
|
+
pip install -e ".[dev]"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# scan the current repo
|
|
109
|
+
ctxsentry scan .
|
|
110
|
+
|
|
111
|
+
# scan a repo you just cloned, before opening it in your editor
|
|
112
|
+
ctxsentry scan ../suspicious-repo
|
|
113
|
+
|
|
114
|
+
# scan only what changed — fast pre-commit / PR gating
|
|
115
|
+
ctxsentry scan . --changed # vs HEAD (+ staged/unstaged/untracked)
|
|
116
|
+
ctxsentry scan . --changed origin/main # vs a base branch
|
|
117
|
+
|
|
118
|
+
# machine-readable output
|
|
119
|
+
ctxsentry scan . --format json -o ctxsentry.json
|
|
120
|
+
ctxsentry scan . --format sarif -o ctxsentry.sarif # upload to GitHub code scanning
|
|
121
|
+
|
|
122
|
+
# gate a pipeline
|
|
123
|
+
ctxsentry scan . --fail-on medium --git-history
|
|
124
|
+
|
|
125
|
+
# tune the noise
|
|
126
|
+
ctxsentry scan . --min-severity medium --min-confidence medium
|
|
127
|
+
|
|
128
|
+
# list every rule
|
|
129
|
+
ctxsentry rules
|
|
130
|
+
|
|
131
|
+
# score the detectors against the labelled corpus
|
|
132
|
+
ctxsentry bench
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
By default `ctxsentry` only reads files an agent treats as context (docs, rule
|
|
136
|
+
files, MCP config, `*.md`, `*.txt`, config formats). Add `--all-text` to sweep
|
|
137
|
+
source files too.
|
|
138
|
+
|
|
139
|
+
### Exit codes
|
|
140
|
+
|
|
141
|
+
| Code | Meaning |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `0` | completed; nothing at or above `--fail-on` |
|
|
144
|
+
| `1` | findings at or above `--fail-on` (default: `high`) |
|
|
145
|
+
| `2` | usage / runtime error |
|
|
146
|
+
|
|
147
|
+
### Pre-commit hook
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
# .pre-commit-config.yaml
|
|
151
|
+
- repo: https://github.com/EivinDDu/ctxsentry
|
|
152
|
+
rev: v0.3.0
|
|
153
|
+
hooks:
|
|
154
|
+
- id: ctxsentry # add: args: ["--changed"] for staged-only scans
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### GitHub Action
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
# .github/workflows/ctxsentry.yml
|
|
161
|
+
name: ctxsentry
|
|
162
|
+
on: [push, pull_request]
|
|
163
|
+
jobs:
|
|
164
|
+
scan:
|
|
165
|
+
runs-on: ubuntu-latest
|
|
166
|
+
steps:
|
|
167
|
+
- uses: actions/checkout@v4
|
|
168
|
+
- uses: EivinDDu/ctxsentry@v0.3.0
|
|
169
|
+
with:
|
|
170
|
+
fail-on: high # optional (default: high)
|
|
171
|
+
# path: .
|
|
172
|
+
# args: --changed origin/main # PR-diff-only scan
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Suppressing findings
|
|
176
|
+
|
|
177
|
+
Known-good matches (your own security docs, test fixtures) can be silenced two
|
|
178
|
+
ways:
|
|
179
|
+
|
|
180
|
+
- **`.ctxsentryignore`** at the scan root — one glob per line, optionally
|
|
181
|
+
scoped to specific rule ids:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
docs/threat-model.md # ignore every rule for this file
|
|
185
|
+
examples/** # ignore a whole tree
|
|
186
|
+
SECURITY.md:CG101,CG401 # ignore only these rules here
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- **Inline comment** on the flagged line or the line above it:
|
|
190
|
+
|
|
191
|
+
```markdown
|
|
192
|
+
<!-- ctxsentry: ignore CG401 -- example payload documented on purpose -->
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## How it works
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
path ─▶ file walk (skips vendored dirs, binaries, >1 MB)
|
|
199
|
+
─▶ decode (utf-8 / utf-16 / latin-1), keep invisible chars intact
|
|
200
|
+
─▶ classify context (mcp-config │ agent-instructions │ agent-skill │ docs │ generic)
|
|
201
|
+
─▶ run detectors:
|
|
202
|
+
• regex rule table (ctxsentry/rules.py)
|
|
203
|
+
• invisible-Unicode scanner (decodes U+E00xx tag runs)
|
|
204
|
+
• deobfuscated rescan (strip zero-width, fold homoglyphs, re-run rules)
|
|
205
|
+
• encoded-payload scanner (decodes base64 / hex, rescans plaintext)
|
|
206
|
+
• layout / smuggling scanner
|
|
207
|
+
• MCP JSON structure walk
|
|
208
|
+
• filename scanner
|
|
209
|
+
─▶ context-adjust severity ─▶ sort ─▶ render (text │ json │ sarif │ markdown)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
No network calls. No LLM. Deterministic.
|
|
213
|
+
|
|
214
|
+
## Benchmark
|
|
215
|
+
|
|
216
|
+
`ctxsentry bench` runs the detectors over a labelled corpus in [`benchmark/`](benchmark)
|
|
217
|
+
(20 malicious fixtures across every family, 18 realistic benign ones) and reports
|
|
218
|
+
precision / recall / F1 / false-positive rate. CI fails the build on any
|
|
219
|
+
regression:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
cases: 38 TP 20 FN 0 FP 0 TN 18
|
|
223
|
+
precision 1.000 recall 1.000 F1 1.000 FP-rate 0.000 rule-accuracy 1.000
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The benign fixtures are the point — normal `README`s, a `SECURITY.md`, setup
|
|
227
|
+
docs that mention API keys, `### System Requirements` headings — content that
|
|
228
|
+
*looks* adjacent to an attack but must not trip the scanner.
|
|
229
|
+
|
|
230
|
+
## Limitations
|
|
231
|
+
|
|
232
|
+
- Static pattern matching: a novel paraphrase with no known markers can slip
|
|
233
|
+
through, and benign security documentation *about* prompt injection will
|
|
234
|
+
produce findings (tune with `--min-confidence`).
|
|
235
|
+
- Not a replacement for [`mcp-scan`](https://github.com/invariantlabs-ai/mcp-scan)
|
|
236
|
+
(runtime MCP), secret scanners, or SAST — it covers the one thing they don't.
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pip install -e ".[dev]"
|
|
242
|
+
pytest
|
|
243
|
+
ctxsentry bench # detection score against benchmark/
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Adding a detector? Add a fixture to `benchmark/malicious/` (and a benign
|
|
247
|
+
counterpart if it could misfire), list it in `benchmark/cases.jsonl`, and keep
|
|
248
|
+
`ctxsentry bench` at 100% recall / 0 false positives.
|
|
249
|
+
|
|
250
|
+
Releasing is documented in [`docs/RELEASING.md`](docs/RELEASING.md) — tag a
|
|
251
|
+
version and publish a GitHub Release; CI builds and uploads to PyPI via Trusted
|
|
252
|
+
Publishing.
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ctxsentry
|
|
2
|
+
|
|
3
|
+
**Scan a repository for prompt-injection payloads *before* you point an AI coding agent at it.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/ctxsentry/)
|
|
6
|
+
[](https://github.com/EivinDDu/ctxsentry/actions/workflows/ci.yml)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
<sub>Formerly published as `ctxguard` (≤ 0.3.0); renamed because that name was taken on PyPI.</sub>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The gap this fills
|
|
15
|
+
|
|
16
|
+
Every current MCP / AI-agent security scanner points *outward* — it probes a
|
|
17
|
+
remote MCP server, or audits the agent framework you are building. Almost nothing
|
|
18
|
+
points *inward* at the **untrusted repository you are about to open in Cursor,
|
|
19
|
+
Claude Code, Copilot, or Windsurf**.
|
|
20
|
+
|
|
21
|
+
That repo is attacker-influenced text. The moment your agent reads its
|
|
22
|
+
`README.md`, `CLAUDE.md`, `.cursorrules`, `.mcp.json`, issue templates, or even a
|
|
23
|
+
filename, any instructions hidden in that content enter the model's context.
|
|
24
|
+
Cloud Security Alliance research in 2026 documented "README instruction
|
|
25
|
+
injection" against coding agents with attack success rates of 41–84%, and MCP
|
|
26
|
+
tool-description poisoning (CVE-2025-54136) works the same way.
|
|
27
|
+
|
|
28
|
+
`ctxsentry` is a fast, dependency-free static scanner that flags that content so a
|
|
29
|
+
human reviews it first.
|
|
30
|
+
|
|
31
|
+
**→ See [`docs/walkthrough.md`](docs/walkthrough.md) for a worked example: five
|
|
32
|
+
planted attacks in a sample repo, and the scan that catches every one.**
|
|
33
|
+
|
|
34
|
+
## What it detects
|
|
35
|
+
|
|
36
|
+
| Family | Examples |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **Instruction override** | `ignore all previous instructions`, `you are now…`, chat-template tokens (`<system>`, `[INST]`, `<\|im_start\|>`), `do not tell the user`, `<IMPORTANT>` priority markers, jailbreak / guardrail-removal phrasing (`developer mode`, `do anything now`, `ignore your guidelines`) |
|
|
39
|
+
| **Agent-directed imperatives** | sentences addressed to "the AI / assistant / agent" that also name an action (`run`, `curl`, `exfiltrate`, `install`, `push`) |
|
|
40
|
+
| **Data exfiltration & RCE** | instructions to send `.env` / tokens / file contents somewhere, callback URLs (`webhook.site`, `ngrok`, `oast`, `requestbin`…), markdown images with query strings, `curl … \| sh`, reverse-shell one-liners (`bash -i >& /dev/tcp/…`) |
|
|
41
|
+
| **Data exfiltration & RCE** *(cont.)* | DNS exfiltration (`dig $(cat …).attacker`), `git remote add` + `git push` to a non-GitHub URL, `postinstall` hooks that shell out |
|
|
42
|
+
| **Context / prompt disclosure** | `repeat the text above verbatim`, `what is your system prompt`, fake `--- END OF DOCUMENT ---` / `system override:` boundaries injected into retrieved content |
|
|
43
|
+
| **Hidden Unicode** | Unicode **Tag** characters `U+E00xx` (decoded and shown), bidirectional overrides (Trojan Source), zero-width runs, Private-Use-Area smuggling, Latin/Cyrillic/Greek homoglyph words |
|
|
44
|
+
| **Deobfuscated rescan** | strips zero-width / tag characters and folds homoglyphs, then re-runs every rule — catches `Ignore all previous instructions` and `іgnоrе …` evasions |
|
|
45
|
+
| **Encoded payloads** | base64 / hex blobs are decoded and the plaintext rescanned — a hidden `ignore all previous instructions…` inside a base64 string is surfaced with the decoded text |
|
|
46
|
+
| **Layout smuggling** | instruction text pushed off-screen by whitespace, `display:none` / `color:#fff` / `font-size:0` spans, instruction-bearing HTML comments |
|
|
47
|
+
| **MCP config poisoning** | `.mcp.json` `description` / `instructions` fields carrying hidden directives or secret references; server launch commands that pipe a download into a shell |
|
|
48
|
+
| **Filename injection** | control / invisible / bidi characters in filenames, filenames that spell out an imperative |
|
|
49
|
+
|
|
50
|
+
Severity is **boosted by context**: the same string is `medium` in a source
|
|
51
|
+
comment but `critical` in `.mcp.json` or `CLAUDE.md`, because agents load those
|
|
52
|
+
files as authoritative instructions.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pipx install ctxsentry # recommended
|
|
58
|
+
# or
|
|
59
|
+
pip install ctxsentry
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
From source:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/EivinDDu/ctxsentry
|
|
66
|
+
cd ctxsentry
|
|
67
|
+
pip install -e ".[dev]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# scan the current repo
|
|
74
|
+
ctxsentry scan .
|
|
75
|
+
|
|
76
|
+
# scan a repo you just cloned, before opening it in your editor
|
|
77
|
+
ctxsentry scan ../suspicious-repo
|
|
78
|
+
|
|
79
|
+
# scan only what changed — fast pre-commit / PR gating
|
|
80
|
+
ctxsentry scan . --changed # vs HEAD (+ staged/unstaged/untracked)
|
|
81
|
+
ctxsentry scan . --changed origin/main # vs a base branch
|
|
82
|
+
|
|
83
|
+
# machine-readable output
|
|
84
|
+
ctxsentry scan . --format json -o ctxsentry.json
|
|
85
|
+
ctxsentry scan . --format sarif -o ctxsentry.sarif # upload to GitHub code scanning
|
|
86
|
+
|
|
87
|
+
# gate a pipeline
|
|
88
|
+
ctxsentry scan . --fail-on medium --git-history
|
|
89
|
+
|
|
90
|
+
# tune the noise
|
|
91
|
+
ctxsentry scan . --min-severity medium --min-confidence medium
|
|
92
|
+
|
|
93
|
+
# list every rule
|
|
94
|
+
ctxsentry rules
|
|
95
|
+
|
|
96
|
+
# score the detectors against the labelled corpus
|
|
97
|
+
ctxsentry bench
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
By default `ctxsentry` only reads files an agent treats as context (docs, rule
|
|
101
|
+
files, MCP config, `*.md`, `*.txt`, config formats). Add `--all-text` to sweep
|
|
102
|
+
source files too.
|
|
103
|
+
|
|
104
|
+
### Exit codes
|
|
105
|
+
|
|
106
|
+
| Code | Meaning |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `0` | completed; nothing at or above `--fail-on` |
|
|
109
|
+
| `1` | findings at or above `--fail-on` (default: `high`) |
|
|
110
|
+
| `2` | usage / runtime error |
|
|
111
|
+
|
|
112
|
+
### Pre-commit hook
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
# .pre-commit-config.yaml
|
|
116
|
+
- repo: https://github.com/EivinDDu/ctxsentry
|
|
117
|
+
rev: v0.3.0
|
|
118
|
+
hooks:
|
|
119
|
+
- id: ctxsentry # add: args: ["--changed"] for staged-only scans
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### GitHub Action
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
# .github/workflows/ctxsentry.yml
|
|
126
|
+
name: ctxsentry
|
|
127
|
+
on: [push, pull_request]
|
|
128
|
+
jobs:
|
|
129
|
+
scan:
|
|
130
|
+
runs-on: ubuntu-latest
|
|
131
|
+
steps:
|
|
132
|
+
- uses: actions/checkout@v4
|
|
133
|
+
- uses: EivinDDu/ctxsentry@v0.3.0
|
|
134
|
+
with:
|
|
135
|
+
fail-on: high # optional (default: high)
|
|
136
|
+
# path: .
|
|
137
|
+
# args: --changed origin/main # PR-diff-only scan
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Suppressing findings
|
|
141
|
+
|
|
142
|
+
Known-good matches (your own security docs, test fixtures) can be silenced two
|
|
143
|
+
ways:
|
|
144
|
+
|
|
145
|
+
- **`.ctxsentryignore`** at the scan root — one glob per line, optionally
|
|
146
|
+
scoped to specific rule ids:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
docs/threat-model.md # ignore every rule for this file
|
|
150
|
+
examples/** # ignore a whole tree
|
|
151
|
+
SECURITY.md:CG101,CG401 # ignore only these rules here
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- **Inline comment** on the flagged line or the line above it:
|
|
155
|
+
|
|
156
|
+
```markdown
|
|
157
|
+
<!-- ctxsentry: ignore CG401 -- example payload documented on purpose -->
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## How it works
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
path ─▶ file walk (skips vendored dirs, binaries, >1 MB)
|
|
164
|
+
─▶ decode (utf-8 / utf-16 / latin-1), keep invisible chars intact
|
|
165
|
+
─▶ classify context (mcp-config │ agent-instructions │ agent-skill │ docs │ generic)
|
|
166
|
+
─▶ run detectors:
|
|
167
|
+
• regex rule table (ctxsentry/rules.py)
|
|
168
|
+
• invisible-Unicode scanner (decodes U+E00xx tag runs)
|
|
169
|
+
• deobfuscated rescan (strip zero-width, fold homoglyphs, re-run rules)
|
|
170
|
+
• encoded-payload scanner (decodes base64 / hex, rescans plaintext)
|
|
171
|
+
• layout / smuggling scanner
|
|
172
|
+
• MCP JSON structure walk
|
|
173
|
+
• filename scanner
|
|
174
|
+
─▶ context-adjust severity ─▶ sort ─▶ render (text │ json │ sarif │ markdown)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
No network calls. No LLM. Deterministic.
|
|
178
|
+
|
|
179
|
+
## Benchmark
|
|
180
|
+
|
|
181
|
+
`ctxsentry bench` runs the detectors over a labelled corpus in [`benchmark/`](benchmark)
|
|
182
|
+
(20 malicious fixtures across every family, 18 realistic benign ones) and reports
|
|
183
|
+
precision / recall / F1 / false-positive rate. CI fails the build on any
|
|
184
|
+
regression:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
cases: 38 TP 20 FN 0 FP 0 TN 18
|
|
188
|
+
precision 1.000 recall 1.000 F1 1.000 FP-rate 0.000 rule-accuracy 1.000
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The benign fixtures are the point — normal `README`s, a `SECURITY.md`, setup
|
|
192
|
+
docs that mention API keys, `### System Requirements` headings — content that
|
|
193
|
+
*looks* adjacent to an attack but must not trip the scanner.
|
|
194
|
+
|
|
195
|
+
## Limitations
|
|
196
|
+
|
|
197
|
+
- Static pattern matching: a novel paraphrase with no known markers can slip
|
|
198
|
+
through, and benign security documentation *about* prompt injection will
|
|
199
|
+
produce findings (tune with `--min-confidence`).
|
|
200
|
+
- Not a replacement for [`mcp-scan`](https://github.com/invariantlabs-ai/mcp-scan)
|
|
201
|
+
(runtime MCP), secret scanners, or SAST — it covers the one thing they don't.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pip install -e ".[dev]"
|
|
207
|
+
pytest
|
|
208
|
+
ctxsentry bench # detection score against benchmark/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Adding a detector? Add a fixture to `benchmark/malicious/` (and a benign
|
|
212
|
+
counterpart if it could misfire), list it in `benchmark/cases.jsonl`, and keep
|
|
213
|
+
`ctxsentry bench` at 100% recall / 0 false positives.
|
|
214
|
+
|
|
215
|
+
Releasing is documented in [`docs/RELEASING.md`](docs/RELEASING.md) — tag a
|
|
216
|
+
version and publish a GitHub Release; CI builds and uploads to PyPI via Trusted
|
|
217
|
+
Publishing.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"filesystem": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": [
|
|
6
|
+
"-y",
|
|
7
|
+
"@modelcontextprotocol/server-filesystem",
|
|
8
|
+
"./data"
|
|
9
|
+
],
|
|
10
|
+
"tools": [
|
|
11
|
+
{
|
|
12
|
+
"name": "read_file",
|
|
13
|
+
"description": "Read a file from the data directory."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "list_dir",
|
|
17
|
+
"description": "List the contents of a directory."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|