diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..42ae98a --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "rhoulam", + "owner": { + "name": "Ismail Rhoulam" + }, + "description": "Open-source software, tools, and experiments from rhoulam.org.", + "plugins": [ + { + "name": "ctxguard", + "source": ".", + "version": "0.1.0", + "description": "Blocks secrets, credentials, and sensitive files before they enter an AI coding agent's context through Claude Code tool calls." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..eb24112 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "ctxguard", + "version": "0.1.0", + "description": "Blocks secrets, credentials, and sensitive files before they enter an AI coding agent's context through Claude Code tool calls.", + "author": { + "name": "Ismail Rhoulam", + "email": "rhoulam.backup@gmail.com" + }, + "homepage": "https://github.com/Ismail-Rhoulam/ctxguard", + "repository": "https://github.com/Ismail-Rhoulam/ctxguard", + "license": "MIT", + "keywords": [ + "claude-code", + "security", + "secrets", + "hooks", + "ai-agent-security", + "llm-security" + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6d3f763 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Install development dependencies + run: python -m pip install -e ".[dev]" + - name: Run test suite + run: python -m pytest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..70af25a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,62 @@ +name: Publish ctxguard to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + name: Build distributions + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install build tools + run: python -m pip install --upgrade build twine + + - name: Build package + run: python -m build + + - name: Validate distributions + run: python -m twine check dist/* + + - name: Upload distributions + uses: actions/upload-artifact@v5 + with: + name: python-package-distributions + path: dist/ + if-no-files-found: error + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + + environment: + name: pypi + url: https://pypi.org/project/ctxguard/ + + permissions: + id-token: write + + steps: + - name: Download distributions + uses: actions/download-artifact@v6 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/README.md b/README.md index 0598d0b..b6d491f 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,189 @@ # ctxguard -**Your AI agent should never see your API keys. This makes sure it doesn't.** +[![CI](https://github.com/Ismail-Rhoulam/ctxguard/actions/workflows/ci.yml/badge.svg)](https://github.com/Ismail-Rhoulam/ctxguard/actions/workflows/ci.yml) +[![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -ctxguard is a Claude Code plugin plus a standalone CLI that stops secrets, credentials, and sensitive files from entering the model's context window. It hooks Claude Code's `PreToolUse` event for `Read`, `Edit`, `Write`, `Bash`, `Grep`, and `Glob`, scans the target before the tool runs, and denies the call when it detects a likely secret. Claude gets a clear, masked explanation instead of your credentials. +**Your AI coding agent should never see your API keys.** -## 30-second install +**ctxguard blocks secrets before they enter the model's context.** + +ctxguard is a local Claude Code plugin and standalone Python CLI. It intercepts +sensitive tool calls before execution and returns a masked explanation instead +of letting likely secrets, credentials, or sensitive files enter context. + + + +## 30-second installation + +### Claude Code plugin (no Python package install) + +Inside Claude Code, add this repository as a marketplace and install ctxguard: + +```text +/plugin marketplace add Ismail-Rhoulam/ctxguard +/plugin install ctxguard@ctxguard-plugins +/reload-plugins +``` + +The plugin includes its own Python source. It requires `python3` 3.9 or newer +on `PATH`, but does not require `pip install ctxguard`. + +For local development, run `claude --plugin-dir .` from this repository. + +### Standalone CLI ```bash -pip install ctxguard && ctxguard init +python3 -m pip install ctxguard +ctxguard init ``` -`ctxguard init` writes a default `.ctxguard.toml` and registers the hooks in `.claude/settings.json` (it asks before touching an existing settings file). Start a new Claude Code session and you are protected. +`ctxguard init` creates `.ctxguard.toml` if absent and registers CLI-backed +hooks in `.claude/settings.json`. If that settings file already exists, ctxguard +asks before modifying it; non-interactive use requires the explicit `--yes` +flag. It merges entries and preserves unrelated settings. + +## Why ctxguard? -Prefer the plugin route? Install this repo as a Claude Code plugin (see [Installing as a plugin](#installing-as-a-plugin)) and skip `init` entirely. +| Tool | Primary protection | +| --- | --- | +| `.gitignore` | Prevents selected files from being committed | +| Gitleaks / TruffleHog | Detects secrets in repositories and commits | +| ctxguard | Blocks sensitive content before an AI coding agent reads it | + +ctxguard complements repository scanners; it does not replace them. Gitleaks +and TruffleHog protect repository history and workflows. ctxguard addresses a +different boundary: the agent tool call immediately before content enters the +model context. ## How it works -Claude Code hooks can allow, deny, or ask about a tool call. They cannot rewrite what a tool returns. ctxguard is therefore **block-and-report, not silent redaction**: when a tool call would pull a likely secret into context, the call is denied with exit code 2 and Claude receives a masked, human-readable reason it can act on (for example, suggesting you add the file to `.gitignore` or move the value to a secrets manager). Nothing is ever redacted in flight, and the raw secret value never appears in any output, log, or report; matches are always masked to the first 4 and last 2 characters. +Claude Code hooks can allow or deny a tool call, but cannot rewrite its result. +ctxguard is therefore **block-and-report, not silent redaction**: + +1. A `PreToolUse` hook inspects `Read`, `Edit`, `Write`, `Bash`, `Grep`, and + `Glob` inputs before execution. +2. Sensitive filenames, small text-file contents, common shell reads, + environment dumps, and inline high-confidence secrets are checked locally. +3. In `block` mode, a flagged call is denied with a masked reason. In `warn` + mode, it is allowed with warning context. +4. A capped `SessionStart` scan tells Claude which files to avoid without + returning their raw contents. + +The CLI uses the same detector module as both plugin hooks. -Three layers: +### Example blocked tool call + +```text +Claude Code attempts: Read .env +ctxguard intercepts: PreToolUse +Tool call result: DENIED +Explanation: ctxguard blocked this Read call: .env is a sensitive file by name +Raw values printed: no +``` -1. **PreToolUse guard**: checks `Read`/`Edit`/`Write` file paths (by filename and, for small text files, by content), `Bash` commands (reads of sensitive files, environment dumps, and secret literals inline in the command), and `Grep`/`Glob` patterns that specifically target sensitive files. -2. **SessionStart scan**: a fast, capped scan of the project at session start that injects a masked summary ("3 potential secrets in 2 files...") so Claude knows which files to avoid. It skips `.git`, gitignored files, binaries, and anything over 1 MB, and is budgeted to finish in well under a second. -3. **Standalone CLI**: `ctxguard scan` audits a whole repo outside any session, with a pretty terminal report or `--json` for CI. Exit code 1 when findings exist, 0 when clean. +Run the synthetic demonstration with `python3 scripts/demo.py`. See +[docs/demo.md](docs/demo.md) for details and safe GIF-recording instructions. ## What it detects - AWS access key IDs and secret access keys -- GitHub personal access tokens (classic and fine-grained) -- Slack tokens (`xoxb-`, `xoxp-`, ...) +- GitHub classic and fine-grained personal access tokens +- Slack tokens - Stripe live and test secret keys -- Private key blocks (`-----BEGIN ... PRIVATE KEY-----`) -- JWT-shaped strings -- `KEY=` / `TOKEN=` / `SECRET=` / `PASSWORD=` assignments with non-trivial values (length, digit, and entropy thresholds filter out placeholders like `TOKEN=your-key-here`) -- High-entropy strings assigned to any variable whose name contains `key`, `secret`, `token`, `password`, or `credential` -- Sensitive files by name alone, regardless of content: `.env`, `.env.*` (except `.env.example` and friends), `id_rsa`, `id_ed25519`, `*.pem`, `*.pfx`, `credentials.json`, `service-account*.json`, and similar +- private key blocks and JWT-shaped strings +- credential-like assignments that pass length, digit, and entropy thresholds +- high-entropy values assigned to names containing `key`, `secret`, `token`, + `password`, or `credential` +- sensitive filenames including `.env`, non-template `.env.*`, SSH keys, + `*.pem`, `*.pfx`, `credentials.json`, and `service-account*.json` -Detection is pattern- and entropy-based, entirely local, with no network calls and no ML. +Detection is heuristic and local. ctxguard makes no network calls and uses no +telemetry or machine learning. ## Configuration -Copy `.ctxguard.toml.example` to `.ctxguard.toml` in your project root (or let `ctxguard init` write one): +Copy `.ctxguard.toml.example` to `.ctxguard.toml`, or run `ctxguard init`: ```toml -# "block" denies flagged tool calls; "warn" allows them but injects a warning. -mode = "block" +mode = "block" # or "warn" -# fnmatch patterns ctxguard should never flag (test fixtures, examples). allowlist = [ "tests/fixtures/*", ] -# Your own patterns, applied everywhere the built-ins are. [[custom_patterns]] name = "acme_internal_token" regex = "acme_[A-Za-z0-9]{32}" confidence = "high" ``` -## CLI usage +Allowlisting suppresses detection and should be narrow. Custom regular +expressions are applied alongside built-ins. + +## CLI ```bash -ctxguard scan # scan the current directory, pretty report -ctxguard scan path/ --json # machine-readable, for CI (exit 1 if findings) -ctxguard init # write config + register hooks in .claude/settings.json +ctxguard scan # scan current directory; exit 1 on findings +ctxguard scan path/ --json # machine-readable masked report +ctxguard init # create config and register Claude Code hooks ctxguard --version ``` -## Installing as a plugin - -The repo is a self-contained Claude Code plugin: `plugin.json` plus `hooks/hooks.json` register the two hook scripts via `${CLAUDE_PLUGIN_ROOT}`, so no pip install is needed inside the plugin. Add it through Claude Code's plugin system (for example from a marketplace entry pointing at this repo), then verify with a new session: ask Claude to read a `.env` file and it should be denied with a ctxguard message, and `/hooks` should list both registrations. - ## Honest limitations -- Pattern and entropy matching cannot catch every secret format. A secret that looks like plain prose will slip through; an unusual but random-looking value might only be caught if its variable name suggests a credential. Treat ctxguard as a strong seatbelt, not a cryptographic guarantee. -- ctxguard blocks tool calls; it does not redact. If a secret is already inside a file Claude legitimately reads (say, buried in an otherwise normal source file under the thresholds), it can still surface. -- The Bash guard is heuristic. It catches common read patterns (`cat .env`, `env | grep ...`, secrets inline in commands), not every possible shell construction. -- Hooks require `python3` on PATH (Python 3.9+; no third-party packages needed for the hooks themselves). +- Pattern and entropy matching has false positives and false negatives. A + secret that resembles prose may pass; an unusual random value may be flagged. +- ctxguard blocks calls; it cannot redact tool output in flight. If a detector + misses sensitive content in a normal-looking file, that content can enter + context. +- The Bash parser is heuristic and cannot model every shell construction. +- Filename checks intentionally deny some files without inspecting content. +- Files over 1 MB and binary files are not content-scanned. Session scans are + time- and file-capped for responsiveness. +- Hook errors fail open so ctxguard cannot break a Claude Code session. This + preserves availability but means a broken hook does not provide protection. +- Native Windows compatibility is not claimed; current testing covers the + Python package and hook contract on Unix-like CI runners. + +Use layered controls: least-privilege credentials, a secrets manager, +`.gitignore`, repository secret scanning, short-lived tokens, and revocation. + +## Security model + +ctxguard trusts the local Python runtime, Claude Code's hook execution, the +installed plugin files, and project configuration. Detection and reporting stay +on the local machine. Reports contain masked matches, filenames, categories, +and line numbers, never intentionally the full matched value. + +An attacker who can alter the plugin, configuration, interpreter, hook input, +or files between inspection and execution may bypass it. ctxguard is a +preventive guardrail, not a sandbox, secrets manager, or formal non-disclosure +guarantee. See [SECURITY.md](SECURITY.md) for private reporting guidance. ## Development ```bash -python3 -m venv .venv && .venv/bin/pip install -e ".[dev]" +python3 -m venv .venv +.venv/bin/pip install -e ".[dev]" .venv/bin/pytest +.venv/bin/python -m build +.venv/bin/twine check dist/* ``` -Scanning this repo with ctxguard flags the files under `tests/fixtures/`: that is expected, they exist to be found. +Test the plugin locally with `claude plugin validate .` and +`claude --plugin-dir .`. Fixtures containing detector-positive synthetic values +are confined to `tests/fixtures/` and allowlisted by the repository example +configuration. ## Contributing -Issues and PRs welcome. Useful contributions, roughly in order of impact: - -- New detector patterns (add the regex to `ctxguard/detectors.py` **with both a positive and a negative fixture** and tests for each) -- False-positive reports: real-world values that got flagged and should not be -- A `--fix` mode that appends flagged files to `.gitignore` - -Keep detection logic in `ctxguard/detectors.py` only; the hooks and the CLI both import from there. Run the full test suite before submitting. +Issues and pull requests are welcome. Detector changes must include positive +and negative tests, and outputs must never reveal an unmasked match. Keep the +project local, dependency-light, auditable, and honest about limitations. Run +the complete test suite before submitting. See [SECURITY.md](SECURITY.md) for +security reports rather than opening a public bypass report. ## License -MIT, see [LICENSE](LICENSE). +MIT. See [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ecd2d4f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security policy + +## Supported versions + +Until the first stable release, security fixes are provided for the latest +`0.1.x` release only. Users should upgrade to the newest patch version before +reporting a problem. + +## Reporting a vulnerability + +GitHub private vulnerability reporting is the preferred channel, but it is not +currently enabled for this repository. Open a minimal public issue asking the +maintainer to arrange a private channel. Do not include vulnerability details +in that issue and do not disclose the vulnerability publicly. Once private +reporting is enabled, use the repository's **Security > Report a vulnerability** +flow instead. + +Never include live credentials in a report. Revoke any credential that may +have been exposed, and reproduce the issue with synthetic or already-revoked +values. Security-sensitive examples and attachments must follow the same rule. + +A useful report includes: + +- affected ctxguard and Claude Code versions; +- operating system and Python version; +- installation method (plugin or Python package); +- the tool name and a sanitized hook input or reproduction; +- expected and actual behavior; +- whether the issue is a detection bypass, false positive, or possible leak; +- logs with all sensitive values removed. + +Detection bypasses and any path that could expose a raw secret are treated as +security-sensitive. Reports will be acknowledged and assessed as quickly as +maintainer availability allows; no fixed response-time guarantee is made. diff --git a/ctxguard/cli.py b/ctxguard/cli.py index 9789cfe..f63ec81 100644 --- a/ctxguard/cli.py +++ b/ctxguard/cli.py @@ -19,7 +19,7 @@ from . import detectors as det DEFAULT_TOML = """\ -# ctxguard configuration. See https://github.com/irhoulam/ctxguard +# ctxguard configuration. See https://github.com/Ismail-Rhoulam/ctxguard # mode: "block" denies flagged tool calls; "warn" allows them but injects a warning. mode = "block" diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 0000000..fbceab8 --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,24 @@ +# Demo + +Run the safe, deterministic demonstration from the repository root: + +```bash +python3 scripts/demo.py +``` + +The script creates a temporary `.env` containing clearly synthetic values, +simulates Claude Code's `Read` tool input, and passes it to the real plugin hook. +The expected result is `DENIED`; the explanation identifies the sensitive +filename while no raw value is printed. The temporary directory is deleted at +the end of the run. + +## Record a terminal GIF later + +1. Use a clean terminal with a readable font and no unrelated environment data. +2. Start a recorder such as VHS or asciinema. +3. Run `python3 scripts/demo.py`. +4. Review every frame to confirm it contains no credentials or private paths. +5. Save the approved asset as `docs/assets/ctxguard-demo.gif` and then add the + visible README reference. + +Do not record a demo using a real `.env` file, even if values appear masked. diff --git a/docs/launch/anthropic-plugin-submission.md b/docs/launch/anthropic-plugin-submission.md new file mode 100644 index 0000000..e7d3eb1 --- /dev/null +++ b/docs/launch/anthropic-plugin-submission.md @@ -0,0 +1,55 @@ +# Claude Code plugin submission draft + +- **Plugin name:** ctxguard +- **Repository:** https://github.com/Ismail-Rhoulam/ctxguard +- **One-line description:** Blocks secrets, credentials, and sensitive files before they enter an AI coding agent's context window. +- **License:** MIT +- **Maintainer:** Ismail Rhoulam + +## Detailed description + +ctxguard is a local Claude Code plugin and standalone Python CLI. Its +`PreToolUse` hook inspects selected file, shell, and search tool inputs before +execution and denies calls that target sensitive filenames or contain likely +credentials. A capped `SessionStart` scan gives Claude a masked list of files to +avoid. The plugin and CLI share the same pattern- and entropy-based detectors. + +## Installation + +```text +/plugin marketplace add Ismail-Rhoulam/ctxguard +/plugin install ctxguard@ctxguard-plugins +/reload-plugins +``` + +Requires `python3` 3.9+ on `PATH`; installing the Python package is not required +for plugin use. + +## Security and privacy + +Detection runs locally. The project makes no network calls and includes no +telemetry. Findings are reported with masked matches. Private vulnerability +reports should use GitHub Security Advisories and must contain only synthetic +or revoked credentials. + +## Limitations + +Detection is heuristic and may miss secrets or flag benign values. ctxguard +blocks calls rather than redacting tool results. Bash inspection is incomplete, +large and binary files are not content-scanned, session scans are capped, and +hook errors fail open. It is not a sandbox, secret manager, or replacement for +repository scanning. + +## Testing instructions + +```bash +python3 -m venv .venv +.venv/bin/pip install -e ".[dev]" +.venv/bin/pytest +python3 scripts/demo.py +claude plugin validate . +claude --plugin-dir . +``` + +The synthetic demo must print `DENIED` and `Raw values printed: no`. No approval +or submission is claimed by this draft. diff --git a/docs/launch/awesome-claude-code.md b/docs/launch/awesome-claude-code.md new file mode 100644 index 0000000..6aaae5e --- /dev/null +++ b/docs/launch/awesome-claude-code.md @@ -0,0 +1 @@ +A local Claude Code plugin and CLI that intercepts tool calls and blocks secrets, credentials, and sensitive files before they enter the model context. diff --git a/docs/launch/reddit.md b/docs/launch/reddit.md new file mode 100644 index 0000000..93c272f --- /dev/null +++ b/docs/launch/reddit.md @@ -0,0 +1,32 @@ +# Reddit launch draft + +## Title + +I built ctxguard — a Claude Code hook that blocks API keys before they enter the model's context + +## Post + +I built ctxguard, an open-source MIT-licensed Claude Code plugin and Python CLI. +It intercepts selected tool calls locally with `PreToolUse` and denies the call +when a sensitive filename or likely secret is detected. The explanation is +masked; the raw match is not intentionally returned to Claude. + +Installation in Claude Code: + +```text +/plugin marketplace add Ismail-Rhoulam/ctxguard +/plugin install ctxguard@ctxguard-plugins +``` + +This is complementary to Gitleaks and TruffleHog. Those tools protect repos and +commits; ctxguard focuses on the moment before an AI coding agent reads content. + +It is heuristic, not a guarantee: false positives and false negatives are +possible, Bash parsing is limited, and hooks fail open on errors. Detection +happens locally with no network calls or telemetry. + +Repository: https://github.com/Ismail-Rhoulam/ctxguard + +Technically useful feedback is welcome, especially sanitized bypass cases, +false positives, hook-contract issues, and detector tests. Please never include +live credentials in a report. diff --git a/docs/launch/show-hn.md b/docs/launch/show-hn.md new file mode 100644 index 0000000..202679d --- /dev/null +++ b/docs/launch/show-hn.md @@ -0,0 +1,37 @@ +# Show HN launch draft + +## Title + +Show HN: ctxguard – Block secrets before Claude Code sees them + +## Submission text + +ctxguard is a local, open-source Claude Code plugin and Python CLI that blocks +likely secrets and sensitive files before selected agent tool calls execute. + +Repository: https://github.com/Ismail-Rhoulam/ctxguard + +## First comment + +I built this after noticing a gap between repository secret scanning and agent +context protection. A file can be untracked and never committed, yet still be +read by an AI coding agent. + +ctxguard registers a Claude Code `PreToolUse` hook for file, shell, and search +tools. The hook examines the tool input locally. On a match it denies execution +and provides a masked reason, so it is block-and-report rather than redaction. +Gitleaks and TruffleHog remain useful for repositories and commits; ctxguard is +intended as a complementary control at a different boundary. + +Limitations are important: matching is heuristic, false positives and false +negatives exist, shell parsing is incomplete, and hook errors fail open. It is +not a sandbox or secrets manager. + +To test it: + +```text +/plugin marketplace add Ismail-Rhoulam/ctxguard +/plugin install ctxguard@ctxguard-plugins +``` + +Or clone the repository and run `python3 scripts/demo.py` for a synthetic demo. diff --git a/docs/releases/v0.1.0.md b/docs/releases/v0.1.0.md new file mode 100644 index 0000000..29afd78 --- /dev/null +++ b/docs/releases/v0.1.0.md @@ -0,0 +1,40 @@ +# ctxguard v0.1.0 + +First public release of ctxguard, a local Claude Code plugin and Python CLI for +blocking likely secrets before sensitive tool calls execute. + +## Highlights + +- Claude Code `PreToolUse` protection for `Read`, `Edit`, `Write`, `Bash`, + `Grep`, and `Glob` +- capped `SessionStart` repository scan with masked context +- standalone `ctxguard scan` and `ctxguard init` commands +- local-only, dependency-light hook detection with no network calls or telemetry +- patterns for AWS, GitHub, Slack, Stripe, private keys, JWT-shaped values, + credential assignments, entropy-based secrets, and sensitive filenames +- block and warn modes, path allowlists, and custom regex patterns through + `.ctxguard.toml` + +## Installation + +Claude Code plugin: + +```text +/plugin marketplace add Ismail-Rhoulam/ctxguard +/plugin install ctxguard@ctxguard-plugins +``` + +Standalone CLI: + +```bash +python3 -m pip install ctxguard +ctxguard init +``` + +## Known limitations + +Detection is heuristic and can produce false positives and false negatives. +ctxguard blocks flagged calls rather than rewriting results. Bash inspection +does not model every shell construction, large and binary files are not +content-scanned, session scans are capped, and hook errors fail open. Python +3.9+ must be available; native Windows compatibility has not been verified. diff --git a/plugin.json b/plugin.json deleted file mode 100644 index 2120567..0000000 --- a/plugin.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "ctxguard", - "description": "Blocks secrets, credentials, and sensitive files from entering Claude's context window: PreToolUse guard for Read/Edit/Write/Bash/Grep/Glob plus a SessionStart repo scan.", - "version": "0.1.0", - "author": { - "name": "Your Name", - "email": "you@example.com" - }, - "homepage": "https://github.com/irhoulam/ctxguard", - "license": "MIT", - "keywords": ["security", "secrets", "hooks", "context"] -} diff --git a/pyproject.toml b/pyproject.toml index ed77ce0..fdda04e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] @@ -7,15 +7,15 @@ name = "ctxguard" version = "0.1.0" description = "Blocks secrets, credentials, and sensitive files from ever entering an AI coding agent's context window." readme = "README.md" -license = { text = "MIT" } -authors = [{ name = "Ismail Rhoulam" }] +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Ismail Rhoulam", email = "rhoulam.backup@gmail.com" }] requires-python = ">=3.9" keywords = ["claude-code", "security", "secrets", "hooks", "ai-agents"] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Topic :: Security", ] @@ -25,13 +25,16 @@ dependencies = [ ] [project.optional-dependencies] -dev = ["pytest>=8.0"] +dev = ["build>=1.2", "pytest>=8.0", "twine>=5.0"] [project.scripts] ctxguard = "ctxguard.cli:main" [project.urls] -Homepage = "https://github.com/irhoulam/ctxguard" +Homepage = "https://github.com/Ismail-Rhoulam/ctxguard" +Repository = "https://github.com/Ismail-Rhoulam/ctxguard" +Issues = "https://github.com/Ismail-Rhoulam/ctxguard/issues" +Documentation = "https://github.com/Ismail-Rhoulam/ctxguard#readme" [tool.setuptools] packages = ["ctxguard"] diff --git a/scripts/demo.py b/scripts/demo.py new file mode 100644 index 0000000..7194af8 --- /dev/null +++ b/scripts/demo.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Safe, deterministic ctxguard demonstration using synthetic values only.""" + +from __future__ import annotations + +import json +import subprocess +import sys +import tempfile +from pathlib import Path + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + hook = root / "scripts" / "pretooluse_guard.py" + fake_values = ( + "AWS_ACCESS_KEY_ID=AKIAEXAMPLEONLY1234\n" + "STRIPE_SECRET_KEY=sk_test_example_not_real\n" + ) + + with tempfile.TemporaryDirectory(prefix="ctxguard-demo-") as directory: + project = Path(directory) + target = project / ".env" + target.write_text(fake_values, encoding="utf-8") + payload = { + "hook_event_name": "PreToolUse", + "tool_name": "Read", + "tool_input": {"file_path": str(target)}, + "cwd": str(project), + } + proc = subprocess.run( + [sys.executable, str(hook)], + input=json.dumps(payload), + capture_output=True, + text=True, + check=False, + ) + + print("1. Claude Code attempts: Read .env") + print("2. ctxguard intercepts: PreToolUse") + print(f"3. Tool call result: {'DENIED' if proc.returncode == 2 else 'ALLOWED'}") + if proc.stdout: + reason = json.loads(proc.stdout)["hookSpecificOutput"][ + "permissionDecisionReason" + ] + print(f"4. Explanation: {reason}") + print("5. Raw values printed: no") + return 0 if proc.returncode == 2 and "EXAMPLEONLY" not in proc.stdout else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..5af0b76 --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,86 @@ +"""Distribution, plugin layout, and safe demonstration tests.""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +from conftest import RAW_SECRETS, REPO_ROOT + + +def test_plugin_manifest_and_marketplace_are_consistent(): + manifest = json.loads( + (REPO_ROOT / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8") + ) + marketplace = json.loads( + (REPO_ROOT / ".claude-plugin" / "marketplace.json").read_text( + encoding="utf-8" + ) + ) + entry = marketplace["plugins"][0] + assert manifest["name"] == entry["name"] == "ctxguard" + assert manifest["version"] == entry["version"] == "0.1.0" + assert entry["source"] == "." + assert not (REPO_ROOT / "plugin.json").exists() + + +def test_hook_commands_resolve_in_plugin_root_with_spaces(tmp_path): + plugin_root = tmp_path / "plugin root with spaces" + plugin_root.symlink_to(REPO_ROOT, target_is_directory=True) + hooks = json.loads((REPO_ROOT / "hooks" / "hooks.json").read_text()) + command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + command = command.replace("${CLAUDE_PLUGIN_ROOT}", str(plugin_root)) + payload = { + "hook_event_name": "PreToolUse", + "tool_name": "Read", + "tool_input": {"file_path": str(tmp_path / ".env")}, + "cwd": str(tmp_path), + } + proc = subprocess.run( + command, + shell=True, + input=json.dumps(payload), + capture_output=True, + text=True, + timeout=30, + ) + assert proc.returncode == 2 + assert json.loads(proc.stdout)["hookSpecificOutput"]["permissionDecision"] == "deny" + + +def test_plugin_hook_runs_without_installed_package(tmp_path): + payload = { + "hook_event_name": "PreToolUse", + "tool_name": "Read", + "tool_input": {"file_path": str(tmp_path / ".env")}, + "cwd": str(tmp_path), + } + env = os.environ.copy() + env.pop("PYTHONPATH", None) + proc = subprocess.run( + [sys.executable, "-I", str(REPO_ROOT / "scripts" / "pretooluse_guard.py")], + input=json.dumps(payload), + capture_output=True, + text=True, + env=env, + cwd=tmp_path, + timeout=30, + ) + assert proc.returncode == 2 + + +def test_demo_denies_without_printing_values(): + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "demo.py")], + capture_output=True, + text=True, + timeout=30, + ) + assert proc.returncode == 0, proc.stderr + assert "Tool call result: DENIED" in proc.stdout + assert "Raw values printed: no" in proc.stdout + assert "AKIAEXAMPLEONLY1234" not in proc.stdout + proc.stderr + assert "sk_test_example_not_real" not in proc.stdout + proc.stderr + for raw in RAW_SECRETS: + assert raw not in proc.stdout + proc.stderr