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:
+177
@@ -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.
|
||||
```
|
||||
Reference in New Issue
Block a user