Chamnan

Context-optimized security plugin for Claude Code that builds architecture maps and redacts secrets.

ドキュメント

chamnan

ชำนาญ (cham-nan) — Thai for the fluency that only comes from doing something again.

A Claude Code plugin that makes a repository know itself, so an agent stops rediscovering it. It builds an index the agent reads instead of scanning files, keeps work state that survives compaction, and accumulates the procedures and tools you keep re-deriving.

It is built for codebases you come back to. On a script you write once, it will cost you more than it returns — see Who this is not for.


The problem it aims at

Most token-saving tools compress what the model writes. Measured on one developer's 34 days of real Claude Code usage, that is the small half:

share of cost
context read in91.2%
output written8.8%

The most popular output-compression plugin advertises 65% savings; JetBrains benchmarked it across 86 tasks and measured 8.5% of output tokens — roughly 0.7% of a bill, with no loss of quality. It does what it says; it is just aimed at the smaller half.

chamnan aims at the other 91%: not by compressing context, but by making most of it unnecessary.

What it does

IndexMAP.md — one line per file, generated from the code. The agent reads the index; it greps the detail; it stops reading the tree.
Data modelTable and model names with a one-line summary, pulled from DDL, migrations and ORM models — instead of a schema dump. Only appears if the repo defines one.
API surfaceMethod, path and handler, pulled from route decorators and OpenAPI documents — instead of the whole spec. Only appears if the repo serves one.
ConfigurationThe environment variable names the repo reads. Names only, never values — and it warns if .env is not gitignored.
DeploymentWhat actually runs, read from Kubernetes, Ansible, Compose, Helm and CI manifests: kinds and names, images, roles, pipelines. A Secret contributes its name and nothing under it.
Stored materialThe non-source trees — scanned paperwork, exports, archives — as counts, sizes and dominant extensions. It exists to stop an agent going to look, which costs far more than the section does. Never opened, never read.
StateSTATE.md — injected at session start, so compaction stops erasing what the last session worked out.
ProceduresSkills the agent writes itself when it hits something complex or repeated. Not a shipped library — a mechanism.
ToolsNotices when the same scratch script is written a third time, and offers to keep it.
MeasurementReports context-per-turn for your repo, before and after. Your number, not ours.
RoutingIts own agents run on a cheap model, because "read this file, write one line" does not need an expensive one.

Every part can be switched off independently in .chamnan/config.json. They do not depend on each other, and they do not have equal evidence behind them — see below.

Install

claude plugin marketplace add ArcticFox2029/chamnan
claude plugin install chamnan@chamnan

Then, in a repository you actually work in:

/chamnan:bootstrap

To try it without installing:

claude --plugin-dir ./chamnan

Evidence

Split by how much weight it can carry. The first tier you can reproduce in your own repo in about ten seconds; the second is one developer's history and is labelled as such.

Reproducible — run chamnan-map and see your own

The index against the source it indexes, on three real repositories:

repolanguagessourceQuick Indexratio
a Python appPython, 33 files306,388 tok1,395 tok0.5%
a JS gameJS + shell + Python, 19 files270,466 tok863 tok0.3%
a small dashboardJS + shell, 12 files19,467 tok596 tok3.1%

Six navigation questions ("where is the shop economy tuned?", "what runs every 10 minutes?", "where are credentials stored?") were answered from the Quick Index alone, 6 out of 6, without opening a source file.

One repository, observed — not a controlled trial

On the repo where this was developed, holding the model constant (Sonnet 5 before and after):

per API callbeforeafter
context carried464,191359,466−22.6%
new material read7,1204,283−39.8%
output written860843−2.0%

The same weeks also brought a model change, different kinds of task, and Claude Code updates of its own. This is an observation on n=1, not a benchmark. chamnan-report computes the same figures for your repository, which is the number that should actually decide anything.

The condition this all depends on

The index is built from each file's opening comment. On the three repos above, 92–100% of files had one — because that codebase requires them. A repo without them gets an index of filenames and function counts, which is worth far less.

chamnan-map prints your coverage every run, and /chamnan:bootstrap offers to fill in what is missing. That is not a footnote; it is the difference between this working and not.

Who this is for

  • Developers on a codebase they will be in for months
  • Testers re-running the same checks
  • Infra and IT work with runbooks and repeated procedures
  • Teams where a new session has to pick up where the last one stopped

Who this is not for

  • One-off scripts and throwaway prototypes. You pay the setup and never collect. Genuinely net-negative; use something else.
  • Writing, chat, fiction, anything without code. There is no structure here for it to index.
  • Repos with no comments and no intention of adding any. The index degrades to filenames.
  • Anyone wanting a token discount without changing how they work. The saving comes from the agent reading an index instead of a tree. If it goes back to reading the tree, nothing is saved.

What it deliberately does not do

  • No shipped skill library. Someone else's procedures do not match your repo. This ships the mechanism that writes yours.
  • No output-style compression. That lane is taken, and it is aimed at the 8.8%.
  • No large CLAUDE.md. A plugin about context cost must not become one. The session-start injection is bounded and reports when it is truncated.
  • No claimed percentage on your bill. It measures yours instead.

Language

chamnan writes the comments and procedures it generates in English by default. Those strings are re-read on every session, and English carries the same meaning in fewer tokens — measured at 1.53x for Thai versus English across three matched sentence pairs.

That figure was measured with a local model's tokenizer, not Claude's. Take it as a direction, not a number: the ratio is real, its exact size on Claude is unverified here.

It is a default, not a rule. A team whose reviewers do not read English is better served by comments they will actually read, and the plugin does not argue:

// .chamnan/config.json
{ "language": "th" }

Or just say so — "write the comments in Thai" is enough, and Claude sets it for you. Nothing else in the plugin changes: replies to you are in whatever language you are speaking, always.

One file, only what applies, and a ceiling

Everything above is a section inside a single MAP.md, not a folder of separate catalogues. A section is written only when the repo actually has that thing — a directory of plain scripts gets a code index and nothing else, no empty headings.

The part of MAP.md above ## Full Detail is what gets injected at session start, so it has a budget: index_token_budget, 3,000 tokens by default, well under 1% of a 1M context window. chamnan-map reports against it and says what to do when a repo exceeds it. This is the rule that stops the plugin becoming the cost it exists to remove — that part is paid on every turn.

Everything below ## Full Detail — function signatures, table columns — is never injected. It is grepped for one heading at a time.

When a repo is large enough that even the index exceeds the budget, it is rolled up by directory rather than truncated. Cutting at a byte offset drops whatever sorts last, so on a 196-file repo everything from roughly s onward disappears from the session with nothing to show that an entire area of the code exists — and the agent greps for it, which is the cost this is meant to remove. The roll-up keeps every directory visible with its file count and a sample, and the full entry for any one of them is still a grep away. Measured on that repo: 8,762 tokens of index became 560, with all seven top-level directories still named.

chamnan-map src game indexes several directories into one map when the whole tree is more than you work in.

Secrets

MAP.md is built by copying source comments, and this README suggests committing it. That combination is a way to publish a password, so it is handled rather than assumed away.

  • Some files are never opened. .pem, .key, .pfx, .crt, id_rsa*, .htpasswd, .netrc, *.db, *.sqlite, and similar are skipped by the scanner outright. .gitignore is not relied on: it is often absent, often wrong, and the cost of being wrong is somebody's private key.
  • Everything written passes a redactor, at one choke point on the finished document rather than at each extractor, so a section added later cannot bypass it. Provider tokens (sk-, ghp_, AKIA…, AIza…, xox…, Stripe, GitLab, npm, JWTs), private-key blocks, credentialed URLs, and password = "…"-style assignments are replaced with <REDACTED>.
  • Environment variables are recorded as names only. No code path here carries a value into the output; values are discarded at parse time.

Verified with a repository seeded with a live-looking Stripe key, a postgres://user:pass@host in a comment, and an RSA private key — none reached MAP.md, while postgres://admin:<REDACTED>@db.internal:5432/main stayed readable, because which database on which host is exactly what an index should tell you.

The redaction patterns are narrow on purpose. Redacting everything high-entropy would eat commit hashes, UUIDs and version strings, and a map full of <REDACTED> is not a map.

This protects chamnan's own output, not your whole session. A plugin hook cannot rewrite what the Read tool returns — PostToolUse exposes only additionalContext and systemMessage — so no plugin can filter what Claude reads from your disk. Anything claiming otherwise is describing a capability Claude Code does not have.

Bulk reads

Before a Read pulls in a lock file, a minified bundle, or a very large file, chamnan says so and suggests grep. It never blocks: the one time someone genuinely needs to read package-lock.json is the one time refusing would be most wrong. Turn it off with warn_on_bulk_reads: false.

It does not strip comments or blank lines from files on the way in — partly because hooks cannot, and partly because comments are the highest-value tokens in a file for a reader trying to understand intent. This plugin's entire index is built out of them.

Keeping the index fresh

chamnan-map --install-git-hook

Opt-in, and it appends to an existing pre-commit hook rather than replacing it. After that the index refreshes on any commit that touches tracked files, and never fails a commit if chamnan errors. Remove it by deleting the block marked # >>> chamnan.

A stale index is worse than no index: it is confidently wrong, and the next session believes it.

Layout

.chamnan/
├── MAP.md          architecture index (generated)
├── STATE.md        work in flight — written at milestones, not every edit
├── config.json     which parts are on
├── skills/         procedures the agent recorded
├── tools/          scripts promoted from scratch
└── logs/           bounded, expires

Commands

/chamnan:bootstrapfirst-time setup: index, coverage, fill comments, baseline
/chamnan:remaprebuild the index after the repo's shape changed
/chamnan:capturerecord a procedure worth keeping
/chamnan:promotekeep a scratch script as a tool
/chamnan:reportshow context-per-turn, before and after
chamnan-map · chamnan-report · chamnan-promotethe same things from a shell
chamnan-peek <file>the shape of one file instead of the whole thing — columns, sheets, members, schema, pages
chamnan-peek <file> --find Xonly the parts that match, with line numbers

Reading an attachment without reading it

The index says a directory holds twelve thousand documents so that nobody goes looking. peek is the other half: when a task genuinely needs one of them, opening it whole is the wrong move and skipping it is also the wrong move.

A 3.5 MB CSV is about a million tokens. Its column list, row count and three sample rows are 108, and for almost every question anyone asks of a CSV that is the answer — 9,455x smaller. A SQLite file gives up its tables and row counts in 39. --find narrows further: the matching rows of a 60,000-row file, with their line numbers, in 240.

Understands CSV/TSV, JSON, ZIP-based formats including .xlsx/.docx/.apk, tar archives, SQLite, PDF (including text extraction via zlib), PNG/JPEG/GIF headers, and plain text. Formats with no standard-library reader — Parquet, Avro, ORC — are identified and measured, and say so rather than guessing. A malformed file reports what went wrong instead of raising.

Tests

python3 tests/run_tests.py

87 checks, no dependencies. The redaction cases are the reason the file exists: every other part of this fails visibly — a wrong map entry sends you to the wrong file and you notice — while a redaction regression fails silently and writes a credential into a file this README tells you to commit.

Both directions are covered throughout. A redactor that replaces everything would pass any "did it hide the secret" test perfectly, so the suite also asserts that commit hashes, UUIDs, RFC numbers and credential-free URLs come through untouched.

Two real bugs were found by writing it: the scratch-repeat threshold was tuned against long scripts and silently ignored the short repeated ones it exists to catch, and a Google API key one character outside the expected length slipped the pattern.

Limitations

  • Python is parsed properly (ast); every other language is read with regex, which will miss unusual declarations. A map is a navigation index, not a compiler front-end — a miss costs one grep. Currently: C, C++, Objective-C, Arduino, C#, Swift, Java/Kotlin, Scala, Go, Rust, Zig, Nim, JS/TS, Dart, Ruby, Elixir, Lua, PHP, shell, Terraform, plus Protobuf and GraphQL schemas.
  • Measured against sixteen real open-source repositories across C, C++, Java, Kotlin, C#, Swift, Go, Rust, Python, Ruby, PHP, Dart, Elixir, Lua and TypeScript, rather than fixtures. Summary coverage on them runs 7-100%, and the low end is real: those projects write a licence header where a description would go. A licence is not a description, so it is not counted as one — which is why these figures are lower, and truer, than the ones this README carried before.
  • Nothing here executes the code it reads.
  • chamnan-report needs history on both sides of installation before it can compare anything.

License

MIT