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.
This commit is contained in:
dev
2026-09-16 11:33:09 +03:00
commit 278ecca018
34 changed files with 5315 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
# 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](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`:
```markdown
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.
```