dev 278ecca018 Add ctxguard: hook-enforced context sanitization
Keeps credentials, company names, personal names and PII out of the model's
context. Enforcement lives in Claude Code hooks rather than in instructions to
the agent: a skill alone cannot protect anything, because by the time the agent
reads a rule the surrounding context has already been sent.

Credentials are removed irreversibly and marked. Entities from a user-supplied
dictionary become stable aliases, rewritten back to real values on their way to
disk and to the shell, so code and commands referring to them still work.

Published from a clean tree; development history is not included.
2026-09-16 11:33:09 +03:00

ctx-tools

A Claude Code marketplace: one repository, plugins under plugins/, each carrying its own skills, hooks and commands. Currently one plugin, ctxguard.

This is the maintainer's document: handing the plugin to someone else, and releases. The usage guide — installation, daily use, the CLI, what to do when a tool call is denied — is the main README.md, in Russian.

Install

/plugin marketplace add /path/to/skills
/plugin install ctxguard@ctx-tools

Handing this to someone else

The plugin travels through the marketplace. The dictionary does not, and must not.

1. The code — the short way

Copy the plugin folder into ~/.claude/skills/:

cp -r plugins/ctxguard ~/.claude/skills/ctxguard

That is the whole step. It loads next session as ctxguard@skills-dir with hooks included — verified by a live run: claude plugin list shows it loaded, ${CLAUDE_PLUGIN_ROOT} resolves, and enforcement fires. No marketplace, no install command, no restart beyond starting a new session.

Two things to know:

  • The name must be free. An installed plugin of the same name wins, and disabling it is not enough — the name stays reserved until claude plugin uninstall. The listing says so explicitly when it happens.
  • Copy the directory, not a symlink into a repository you then move.

Use this for one machine or one colleague. Use the marketplace below when several people need the same version and you want updates to be a command rather than a recopy.

2. The code — the maintainable way

Push this repository somewhere the other person can reach, then they run:

/plugin marketplace add <owner>/<repo>          # GitHub shorthand
/plugin install ctxguard@ctx-tools

claude plugin marketplace add also takes a git URL or a filesystem path, and --sparse .claude-plugin plugins limits the checkout inside a monorepo. Nothing is fetched at runtime and there are no dependencies beyond python3.

For a whole team, declare it at project scope and commit the result, so cloning the repo is enough:

claude plugin marketplace add <owner>/<repo> --scope project
claude plugin install ctxguard@ctx-tools --scope project

Releases, and the trap in them

Because plugin.json carries a semver version, claude plugin update compares versions, not commits — a release that is not version-bumped never reaches an installed copy, silently. So every release is:

# bump version in plugins/ctxguard/.claude-plugin/plugin.json AND the marketplace entry
git commit && git push
claude plugin tag plugins/ctxguard        # validates the two agree, tags ctxguard--v1.0.1
git push origin refs/tags/ctxguard--v1.0.1

The recipient then runs claude plugin update ctxguard@ctx-tools and restarts.

Iterating locally is the same trap in miniature: hooks execute the installed copy, not the working tree, so claude plugin marketplace update ctx-tools followed by claude plugin update ctxguard@ctx-tools is required after every change — and a change without a version bump will not be picked up. Confirm with diff -rq ~/.claude/plugins/cache/ctx-tools/ctxguard/<version>/scripts plugins/ctxguard/scripts.

3. The dictionary — copied, never re-derived

~/.claude/ctx-guard/ is per-machine and deliberately outside every repository. It must be handed over explicitly, because aliases cannot be reproduced:

  • they are assigned in registration order, so whoever adds two companies in the other order gets the mapping swapped — two people would mean different companies by CTXG_COMPANY_A;
  • the salt is generated per store, so secret markers and PII aliases do not match either, and findings cannot be correlated across a team.

There is a test asserting exactly this, so the guidance cannot quietly rot. Copy it:

ctxguard entity export --out ~/team-dict.json     # refuses to write inside a git repo
# transfer over a channel you would send the real values over -- it contains them
ctxguard entity import team-dict.json             # run from the project directory

After importing, both machines produce byte-identical output, markers and PII aliases included.

4. What the recipient still has to do

ctxguard init            # create state, show what is active
ctxguard scan .          # what this repository actually holds
ctxguard verify          # 43 canary cases through the real hook entry points
ctxguard scan-transcript # what has actually reached the model

Until entities are registered or imported, only credential and PII detection is live — company names, people and hostnames are not protected. And sanitize CLAUDE.md by hand: the harness loads it directly, so no hook ever sees it.

Layout

.claude-plugin/marketplace.json     marketplace manifest
plugins/ctxguard/                   context sanitization: skill + hooks + CLI
docs/specs/                         design document (historical)
Makefile                            the single entry point for checks

Where does a new skill go?

The skill needs… Put it in
hooks, slash commands, MCP servers, scripts its own plugin under plugins/<name>/, plus a marketplace.json entry
nothing but instructions a bucket plugin: plugins/<bucket>/skills/<name>/SKILL.md

Hooks cannot ship inside a bare skill, which is why this repository is a marketplace rather than a flat directory of SKILL.md files. Instruction-only skills belong in one bucket plugin — a single install covers all of them, whereas plugins that install hooks are added deliberately, one at a time. There is no such bucket here yet; create it along with the first skill that needs it, not in advance.

Conventions, following the installed plugin ecosystem:

  • SKILL.md frontmatter is name + description, third person, trigger-heavy
  • details go into references/*.md, not into SKILL.md
  • scripts are Python 3 stdlib only, invoked as python3 "${CLAUDE_PLUGIN_ROOT}/..."

Plugins

ctxguard — context sanitization

Keeps credentials, client and company names, personal names and PII out of the model's context. Credentials are removed irreversibly; names and PII become stable aliases that are translated back on their way to disk, so the agent stays able to do real work.

Enforcement is in hooks, not in the skill — the agent cannot switch it off, and its forgetting the rules is harmless.

make test        # engine unit tests
make verify      # canary corpus through the real hook entry points
make leaks       # scan transcripts for values that actually reached the model

Start here: plugins/ctxguard/skills/context-sanitization/SKILL.md, then references/threat-model.md for what it deliberately does not cover.

Optional, so the expectation survives the plugin being disabled — add to your CLAUDE.md:

Sensitive data in this project is aliased (`CTXG_*`) and enforced by ctxguard hooks.
Use aliases verbatim, never reconstruct real values, and read a denial's reason
instead of working around it. See the context-sanitization skill.
S
Description
No description provided
Readme
139 KiB
Languages
Python 99.4%
Makefile 0.6%