jcodemunch-mcp indexes source code from local folders and GitHub repositories. This document describes the security controls that protect against common risks when handling arbitrary codebases.
Report privately through GitHub's private vulnerability reporting, which is enabled on this repository: open the Security tab and choose Report a vulnerability, or go straight to https://github.com/jgravelle/jcodemunch-mcp/security/advisories/new. Do not open a public issue for a suspected vulnerability.
- Contact: the report goes to the maintainer, @jgravelle, and stays private between you and the maintainer until an advisory is published.
- What helps: the version you ran, the steps or input that show the problem, and what an attacker could do with it.
- What to expect: an acknowledgement within 3 days and a verdict (confirmed, not a vulnerability, or needs more information) within 14 days. A confirmed report ships as a fix release under the project's normal cadence, and the release is not held back for other findings.
- Credit: you are credited in the advisory,
CHANGELOG.mdand the release notes, unless you ask otherwise. - Scope: the
jcodemunch-mcppackage on PyPI, this repository's workflows, and the published container of the MCP registry entry. The pinned benchmark corpora and the observatory are out of scope.
Every advisory published for this project, with the version that fixed it. Dates are UTC. "Fixed" is the day the fixed version was uploaded to PyPI.
| Advisory | Severity | Affected | Fixed in | Reported | Fixed | Reported by |
|---|---|---|---|---|---|---|
| GHSA-876x-crhg-hp8h | High | >= 1.108.94, <= 1.108.393 | 1.108.394 | 2026-10-21 | 2026-10-23 | @3099404236 |
| GHSA-xxx4-frv5-chp2 | Low | <= 1.108.380 | 1.108.381 | 2026-10-08 | 2026-10-19 | @mich3333 |
To be safe from both, run 1.108.394 or later.
All user-supplied paths are validated before any file is read or written.
validate_path(root, target)resolves both paths to absolute form and verifies the target is a descendant ofrootusingos.path.commonpath().- Applied during file discovery and again before each file read (defense in depth).
- Paths such as
../../etc/passwdor absolute paths outside the repository root are rejected.
Symlinks can be used to escape the repository root and read arbitrary files.
- Default:
follow_symlinks=False— symlinks are skipped during file discovery. - When symlinks are followed (
follow_symlinks=True), each symlink target is resolved and validated against the repository root. Escaping symlinks are skipped with a warning. is_symlink_escape(root, path)checks whether a symlink resolves outside the root.- On Windows, environments without symlink support automatically skip symlink traversal.
Files are filtered through multiple layers:
- SKIP_PATTERNS — directories and files always excluded (e.g.,
node_modules/,vendor/,.git/,build/,dist/, generated files, lock files). .gitignore— respected by default for both local folders and GitHub repositories (via thepathspeclibrary).extra_ignore_patterns— user-configurable additional gitignore-style patterns passed to indexing tools.
Credential files are excluded during indexing by a structured classifier
(secret_classifier.classify_secret_file, behind the security.is_secret_file
boolean). It decides from the filename and directory shape only — it never
reads file contents. Detection is organized into groups, in precedence order:
- Exact credential names:
.env,.env.*,.htpasswd,.netrc,.npmrc,.pypirc,credentials.json,service-account*.json,client_secret*.json,application_default_credentials.json,*-firebase-adminsdk-*.json,token.json,*.token,*.credentials,*.secrets,*.agekey, and SSH private keys (id_rsa*,id_ed25519*,id_dsa*,id_ecdsa*). - Credential extensions:
*.pem,*.key,*.p8,*.p12,*.pfx,*.ppk,*.jks,*.keystore. - Path-specific credentials:
~/.aws/credentials,~/.kube/config,~/.docker/config.json,~/.config/gcloud/application_default_credentials.json,~/.azure/accessTokens.json,~/.cargo/credentials[.toml],~/.gem/credentials,composer/auth.json— credential stores that only a path (not a basename) names. - Key-material directories: a private-key/keystore file under
keys/,certs/,ssl/,tls/,pki/, … (private-key extensions only — public certs*.crt/*.cer/*.derare not excluded just for living here). - Secret-store data: data/config files (
*.yaml,*.json,*.tfvars,*.tfstate[.backup], …) under a whole-segmentsecret/secrets/credential/credentials/creds/vaultdirectory. - Broad
secretbasename: asecret/secretstoken at a word boundary (soprod-secrets.yamlis excluded butsecretariat.csvis not), excluding source-code and documentation files (secret_redaction.pyis code that handles secrets, not a credential file).
Not excluded: public SSH keys (*.pub), public certificates, template /
example fixtures (*.example, *.sample, *.template, *.tmpl, *.dist).
Overrides (exclude_secret_patterns config key): a group slug (e.g.
key_material_directories) disables that whole group; the legacy *secret*
token disables the broad-basename and secret-store groups; any other glob is a
per-pattern allow (a matching file is never treated as secret).
When a secret file is detected, a warning is included in the indexing response. Secret files are never stored in the index or cached content directory.
Secret exclusion above keeps credential files out of the index. Secret redaction is the second, independent control: before any tool response leaves the server, it is swept for credential-shaped strings and matches are masked, so a secret that reached the index some other way does not reach the model's context window.
It runs in the central call_tool dispatcher, so it covers every tool by
construction rather than by each tool remembering. Patterns cover AWS access and
secret keys, GCP service-account keys, Azure storage keys and client secrets,
JWTs, bearer tokens, GitHub tokens and fine-grained PATs, Anthropic, OpenAI and
Slack keys, PEM private keys, generic api_key-shaped assignments, and private
IPv4 addresses.
Default: enabled. Set JCODEMUNCH_REDACT_RESPONSE_SECRETS=0 to disable it.
get_file_content, get_symbol_source and get_context_bundle are skipped
(server.py, _SOURCE_DUMP_TOOLS). These are the tools whose job is to return
raw cached source, and the exemption is a considered tradeoff on two grounds: a
per-byte regex sweep over payloads that can run to hundreds of KB is latency
spent for no gain, and anything those tools return is the user's own checked-in
code being read back to them, not a credential crossing a boundary it had not
already crossed.
⚠ The consequence, stated plainly, because it is the gap a reader needs: a credential hardcoded inside an ordinary source file is caught by neither control. Not by the filename classifier, because the filename is ordinary; not by the redactor, because those three paths are exempt. If that matters in your environment, the mitigations are the ones you would use anyway — secret scanning in CI, and pre-commit hooks — because the credential is in your repository regardless of what this server does with it.
This exemption is a performance and scope decision, not a claim that source files never contain secrets.
- Default maximum: 500 KB per file (configurable via
max_file_sizein config, theJCODEMUNCH_MAX_FILE_SIZEenvironment variable, or themax_sizeargument on a singleindex_folder/index_repocall). - Files exceeding the limit are skipped during discovery, and the indexing response names them in
warnings. A skipped file is treated as withheld, not excluded: coverage reportscomplete: falseand absence claims over the corpus are refused, because "we never read that file" and "that symbol does not exist" are different answers. - A configurable file count limit (default: 500 files) prevents runaway indexing of extremely large repositories. Can be overridden using the
JCODEMUNCH_MAX_INDEX_FILESenvironment variable.
- jCodeMunch honours the Cache Directory Tagging Specification. A directory containing a
CACHEDIR.TAGfile whose first 43 bytes areSignature: 8a477f597d28d172789f06886806bc55is pruned from the walk along with everything beneath it. - The signature is verified. A file merely named
CACHEDIR.TAGexcludes nothing. - The rule is declared by the writer of a directory rather than listed by us, so a tool that writes derived data into your tree is honoured without jCodeMunch knowing its name, and it applies to cache directories that are not dot-directories.
- Pruned directories are counted as
cache_dirindiscovery_skip_counts. This is an ordinary exclusion, not a withholding: a tagged directory is regenerable derived data by its own declaration, so absence claims over the remaining corpus stay citable. - Disable with
respect_cachedir_tag: falseorJCODEMUNCH_RESPECT_CACHEDIR_TAG=0if you tag a directory you nonetheless want indexed. - Applies to local folder indexing. GitHub repository indexing does not honour the tag, because validating the signature requires the file's content and the tree listing carries only paths and sizes.
- A directory whose
pyvenv.cfgcarries ahome =key (PEP 405, written by every venv tool) is a virtualenv root, whatever it is called. Its installed packages, thesite-packagesdirectory atLib/site-packagesorlib/pythonX.Y/site-packages, are pruned from the walk. - Only
site-packagesis pruned. The venv root,bin/,Scripts/,lib/and anything you keep there are still indexed, including a venv created in a project root beside your ownbin/andlib/. - A file merely named
pyvenv.cfgwith nohomekey prunes nothing. - Pruned directories are counted as
virtualenvindiscovery_skip_counts. This is an ordinary exclusion, not a withholding, so absence claims over the remaining corpus stay citable. - Disable with
skip_virtualenvs: falseorJCODEMUNCH_SKIP_VIRTUALENVS=0. Applies to local folder indexing only.
Binary files are excluded using a two-stage check:
- Extension-based detection — common binary extensions (
.exe,.dll,.so,.png,.jpg,.zip,.wasm,.pyc,.class,.pdf,.db,.sqlite, etc.). - Content-based detection — files containing null bytes within the first 8 KB are treated as binary and skipped, even if the extension suggests source code.
- All file reads use
errors="replace"to substitute invalid UTF-8 bytes with the Unicode replacement character (U+FFFD) instead of raising decode errors. - Symbol content retrieval also uses
errors="replace"to ensure safe decoding. - Cached raw files are stored using UTF-8 encoding.
jcodemunch reads the files it indexes as text and parses them with tree-sitter
and regular expressions. It never imports, evaluates or executes them, and no
file in the package calls eval or exec or builds a JavaScript function from
a string. tests/test_shipped_text_has_no_dynamic_code_calls.py scans every
file the sdist ships for that call text.
One exception you switch on: LSP enrichment (enrichment.lsp_enabled in
config.jsonc, off by default). It starts the language server for each
indexed language (pyright, typescript-language-server, gopls,
rust-analyzer), and each server follows its own rules. jcodemunch starts
rust-analyzer with build scripts, procedural macros and check-on-save turned
off. That closes the paths its defaults use to run a project's code, not every
path: it still runs cargo to read the workspace, and a repository can steer
the toolchain through its own configuration files. The other servers may load
code a project names as well. Turn LSP enrichment on only for repositories
you trust, for every server including rust-analyzer.
The words eval and exec still appear as data: names the search_ast
security detector and other checks look for in your code, language keywords
the parsers recognise (Tcl's namespace eval), and ast.literal_eval, which
parses Python literals and runs nothing. Anything jcodemunch starts that
persists or touches the network is described under "Background behavior,
fully disclosed" below.
- Index storage defaults to
~/.code-index/. - The storage path can be overridden using the
CODE_INDEX_PATHenvironment variable. - Every index on the machine shares that one directory. A session lists only the indexes of the folder its server was started in (
repo_visibility, default"workspace"). That scopes what an agent is shown, not who can read: an exact repo id still loads any index, and any process that can read the directory can read every index in it. - Repository identifiers are derived from
{owner}-{name}, preventing path injection in storage locations. - Index files are stored as JSON and validated during load to ensure schema integrity.
GitHub release artifacts (wheel + sdist) are signed with
sigstore-python via a
GitHub Actions workflow (.github/workflows/sign-release.yml) triggered
on release.published. The workflow uses GitHub's OIDC identity as the
signer, so verification ties an artifact back to the specific workflow
in this repository that signed it — no long-lived signing keys, no
external trust roots beyond the Sigstore public-good infrastructure.
Verifying a release (Sigstore v3 bundle format, .sigstore.json):
TAG=v1.108.22 # or whichever release you want to verify
WHEEL=jcodemunch_mcp-${TAG#v}-py3-none-any.whl
BASE="https://github.com/jgravelle/jcodemunch-mcp/releases/download/${TAG}"
curl -L -o "${WHEEL}" "${BASE}/${WHEEL}"
curl -L -o "${WHEEL}.sigstore.json" "${BASE}/${WHEEL}.sigstore.json"
python -m pip install sigstore
python -m sigstore verify github \
--bundle "${WHEEL}.sigstore.json" \
--repository jgravelle/jcodemunch-mcp \
--workflow-name "Sign release artifacts" \
"${WHEEL}"The trust shape is the same one PyPI's PEP 740 attestation pipeline uses: the workflow runs in GitHub Actions, presents an OIDC identity claim to Sigstore's transparency log, and the signature is recoverable from the log via the bundle. Forward-only — releases prior to the signing workflow's introduction don't carry signatures and aren't going to be retroactively resigned.
The following user-writable files participate in the server's trust chain. A process that can write any of them can influence the behavior of every subsequent MCP session: prompt context the agent sees, tool descriptions, hook commands, and which MCP server gets launched. Endpoint-management teams and hardened install templates should treat them with the same care as any other piece of developer configuration that steers an AI agent.
~/.code-index/config.jsonc— global server configuration. Settings here influence tool tier visibility, language gating, secret-pattern lists, and per-tool description overrides.~/.code-index/and everything under it — the symbol index, the optional telemetry SQLite, the bundled-encoder model directory, and the serialized session journal. Bodies cached here are a second copy of every indexed source file../.jcodemunch.jsonc(per-project) — same key shape as the global config, scoped to the directory it lives in. Overrides only those keys it sets.~/.claude/CLAUDE.md,./CLAUDE.md,AGENTS.md,.cursor/rules/jcodemunch.mdc,.windsurfrules— agent-policy files thatjcodemunch-mcp initwrites only when asked (--policy, or--claude-md SCOPE). Each is rendered into the agent's prompt at session start by the corresponding client.init --remove-guidancestrips the jcodemunch block from each, backing the file up first.~/.claude/settings.json(PreToolUse / PostToolUse / PreCompact / SessionStart / TaskCompleted / SubagentStart / WorktreeCreate / WorktreeRemove hooks) — a defaultinitregisters PostToolUse (re-index after an edit), PreCompact and TaskCompleted, none of which put text in front of the model. PreToolUse, SessionStart and SubagentStart, which do, need--guidance(PreToolUse also comes with--strict); the worktree hooks need--hooks(install <agent>passes it). The hook commands run every relevant tool call in the host agent. With the PreToolUse hook (--guidanceor--strict) andJCODEMUNCH_HOOK_GREP_ANSWER=1(off by default), the PreToolUse hook answers a Grep inside an indexed repo from the index: it runssearch_textlocally (a 4-second budget), refuses the Grep with that page as the reason, and lets the same Grep through when it is sent again. It records which Greps it answered, as hashes of the session id and the Grep's arguments, in~/.code-index/_grep_answered.json(the last 200). Nothing leaves the machine.~/.code-index/<repo>.textfilter— a SQLite file beside each index holding a small trigram bitmap per cached file, with that file's size and mtime. Indexing writes it, andsearch_textrepairs rows for files it reads without one. It holds hashes of trigrams, not text, and lets a substring search skip files that cannot match. Deleting the index deletes it; deleting the file only makes searches read every file again..github/hooks/hooks.json— analogous hook surface for GitHub Copilot CLI / cloud agent flows.- Generated MCP client config files (paths depend on which clients are
installed):
~/Library/Application Support/Claude/claude_desktop_config.json(macOS Claude Desktop),%APPDATA%\Claude\claude_desktop_config.json(Windows Claude Desktop),~/.cursor/mcp.json,~/.continue/config.json, and the project-scope.mcp.jsonwritten byclaude mcp add. Each contains the command line Claude / Cursor / Continue spawn to launch the MCP server.
File-integrity monitoring at the endpoint level (SentinelOne, Tanium, etc.) applied to these paths is a reasonable defense-in-depth control in any managed-endpoint deployment.
jcodemunch-mcp watch-install registers a login-time service that watches
indexed directories for filesystem changes and reindexes incrementally. This
is opt-in and reversible (watch-uninstall) but appears in endpoint hunts
that enumerate startup items, so document it as expected when the service is
present:
- Linux (systemd user units):
~/.config/systemd/user/jcodemunch-watch.service. Enabled withsystemctl --user enable --now jcodemunch-watch.service. - macOS (launchd LaunchAgent):
~/Library/LaunchAgents/us.gravelle.jcodemunch-watch.plist. Loaded withlaunchctl bootstrap gui/$UID <plist>. - Windows (Task Scheduler entry): task named
jcodemunch-watchunder the current user, configured to run at logon.
The service runs jcodemunch-mcp watch-all, which performs no network I/O
and only writes back to the per-repo SQLite stores under ~/.code-index/.
get_symbol_source(verify=True) hashes the retrieved source and compares
against the content hash stored in the index. Both values are derived from
the local cache directory, so the default verification is self-referential:
a coherent tamper of ~/.code-index/<repo>/ is durably trusted after
the tamper. Treat the cache directory accordingly — see the security-sensitive
files section above for why it's worth file-integrity monitoring.
Externally-attested verification is available via the
verify_against="git_sha" parameter on get_symbol_source: when set, the
cached source is compared against the working-tree git HEAD slice of the
same file, not against the cache's own stored hash. The response includes
a git_sha_verification field with one of:
git_sha_match— the cached source matches the HEAD slice.git_sha_mismatch— the file exists in HEAD but the slice differs.git_unavailable— the file isn't in HEAD, git is unreachable, or the source isn't a git working tree.
Default remains verify_against="cache" for back-compat. For
managed-endpoint or supply-chain-conscious deployments where cache
integrity matters, the git_sha mode is the externally-attested signal;
the cache mode alone is best read as "the cache is internally
consistent," not "the cache matches the upstream source."
The performance and ranking telemetry introduced in v1.74.0–v1.80.0 is local-only and opt-in:
~/.code-index/telemetry.db(tool_calls,ranking_events) is written only whenperf_telemetry_enabled: true(orJCODEMUNCH_PERF_TELEMETRY=1). Default is disabled — the in-memory latency ring is always tracked but no row touches disk.~/.code-index/tuning.jsonc(per-repo retrieval-weight overrides) is written only by an explicittune_weightsinvocation.~/.code-index/embed_canary.json(16-string drift canary) is written only by an explicitcheck_embedding_drift(capture=true)invocation.- No telemetry is sent over the network. The community token-savings
counter (
share_savings) is unrelated and sends exactly three fields: an integer delta, an integer lifetime total, and an anonymous UUID — never query strings, paths, repo names, or any configuration value. Disable withJCODEMUNCH_SHARE_SAVINGS=0. - Stored ranking events include the literal query string (truncated result-id list, no source code). Treat the storage path with the same care as any local source you index.
Nine GitHub Actions workflows (.github/workflows/inbound-*.yml) read
public issues and pull requests and, when the repository variable
INBOUND_ENABLED is exactly true, label them, draft replies for a
human to approve, evaluate dependency updates, self-check agent-authored
pull requests, attempt a fix when a maintainer applies agent-fix, and
post a weekly digest. docs/inbound/POLICY.md is the full contract. The
controls that matter to a reporter or a contributor:
- Your text is data. Every job treats issue and PR content as input to analyse, never as an instruction; a pre-model scan escalates anything that looks like one (hidden text, authority claims, requests to fetch or post). Nothing from an item is executed, fetched, or pasted into a shell.
- The model cannot write to this repository. The job that runs a
model holds a read-only token and writes one file; a separate job with
no model verifies that file and writes with a GitHub App that a
repository ruleset (part of the layer's setup,
docs/cicd/RUNBOOK.mdsection 9) confines toinbound/**branches and theinbound-ledgerbranch. - No headless job merges, tags, publishes, closes, or edits workflows,
secrets, branch protection,
SECURITY.md, or the quality gates. - A security report is never summarised. An issue classified as security is labelled and escalated to a human by number only; no excerpt reaches a comment, a digest, or the audit ledger. Report vulnerabilities privately, below, not in a public issue.
- Everything is audited. Every run writes a record (job, model,
prompt version, decision, actions, outcome) to the
inbound-ledgerbranch. The switch is off by default and fails closed.
| Control | Location | Default |
|---|---|---|
| Path traversal validation | security.validate_path() |
Always enabled |
| Symlink escape protection | security.is_symlink_escape() |
Symlinks skipped by default |
| Secret file exclusion | security.is_secret_file() |
Always enabled |
| Response secret redaction | redact.redact_dict() in the call_tool dispatcher |
Enabled; JCODEMUNCH_REDACT_RESPONSE_SECRETS=0 disables. Exempt: get_file_content, get_symbol_source, get_context_bundle |
| Binary file detection | security.is_binary_file() |
Always enabled |
| File size limit | File discovery pipeline | 500 KB |
| File count limit | File discovery pipeline | 10,000 files per index_repo (max_index_files), 2,000 per index_folder (max_folder_files); each has an env override |
.gitignore respect |
Indexing pipeline | Enabled |
| UTF-8 safe decode | All file reads | errors="replace" |
| Perf telemetry sink | perf_telemetry_enabled |
Disabled (opt-in) |
| Ranking ledger storage | perf_telemetry_enabled |
Disabled (opt-in) |
| Tuning overrides | Explicit tune_weights call |
None until invoked |
| Embedding canary | Explicit check_embedding_drift call |
None until invoked |
Everything jCodeMunch does beyond answering a tool call is listed here. All of it is visible, opt-in or opt-out, and reversible.
-
File watching. The
watch/watch-all/watch-claudecommands (andwatch: truein config) re-index files when they change. Watching runs inside a process you started and stops when that process exits. Nothing monitors your filesystem unless a jCodeMunch process you launched is running. While it watches a folder inside a git repository, the watcher also looks at that repository's git directory about once a second: it reads the.gitpointer file andcommondirwhere the folder is a linked worktree or a submodule (at most 4096 bytes of each), andHEADand the one ref fileHEADnames (at most 256 bytes of each), and it takes the size and modification time ofpacked-refsandreftable/tables.list. When the commit has moved with no file event it runsgit rev-parse HEADand an incremental re-index of the folder (#1040). It opens no other file under the git directory, writes nothing there, and performs no network I/O. -
Login service — explicit opt-in only.
jcodemunch-mcp watch-installregisterswatch-allas a login service (Windows Task Scheduler / macOS launchd / Linux systemd) so indexes stay fresh across reboots. This happens only when you runwatch-installyourself;init,install, and normal server use never register a service. On Windows the task is registered from an XML definition that turns off Task Scheduler's battery rules and its time limit: it starts and keeps running on battery power, and it is never ended for running too long, so it runs from logon until you log off, stop it, or remove it. It runs as you, at the limited run level, one instance at a time. A task installed by an earlier version keeps Task Scheduler's defaults (no start on battery, stop when the machine goes on battery, end after 72 hours) until you runwatch-installagain. Inspect it withwatch-status, which on Windows also says when the task exists but no watcher is running from it, and when the task still carries the old defaults; remove it withwatch-uninstall. -
Anonymous savings telemetry. The server periodically sends a random anonymous ID plus aggregate token-savings counters to the project's public community meter. No code, no file paths, no repo names, no PII — counters only, and no per-tool detail — the payload is exactly
{delta, total, anon_id}. The meter's receiver is in this repository atserver/savings/post.php: for the integer lifetimetotalthis client sends, it keeps the larger of that and the stored total (atotaloutside its accepted range is refused and stores nothing), and it addsdeltaonly when a request carries no usabletotal, as an older client's does. The sender is a single background daemon thread that starts lazily on the first share (never at import, and never if you have opted out), so a plain import has no background side effect. Opt out withshare_savings: falseinconfig.jsoncorJCODEMUNCH_SHARE_SAVINGS=0; redirect the endpoint withJCODEMUNCH_TELEMETRY_URL. -
One-time tool-surface notice. On server start, if your
tool_surfaceis not the surface a new install would receive today, one WARNING line is written to the log channel naming what a switch would save on your install and the two commands that accept or silence it. It is emitted once per install and never again: a marker at~/.code-index/surface_offer_state.jsonrecords that it was said (a timestamp, the surface, the version, and the pid and transport of the process that said it — nothing else). Nothing is sent anywhere, nothing is changed, and your config is not written; the notice exists becausetool_surfaceis written once at install time and no upgrade can revisit it, so an install created before the current default would otherwise never learn there is a choice. Silence it permanently withjcodemunch-mcp config set surface_offer_seen true, or delete~/.code-index/to erase the marker. -
The same notice, once, in a tool answer. A user inside an MCP client never sees the server's log, so the line above reaches nobody there. The first successful
resolve_repoanswer an install makes therefore carries the same offer as asurface_noticeobject: how many tool-list tokens a switch would take out of every turn's input on your install (read from cache after the first turn, so the cost saving is smaller than the count), the commands that switch, undo and silence it, and a line asking the agent to tell you once and not to change the setting itself. It is added once per install: a marker at~/.code-index/surface_notice_state.jsonrecords that it was said (a timestamp, the tool, the surface and the version, nothing else). It can be said a second time if the marker cannot be written, or if two servers answer their first call at the same moment. Nothing is sent anywhere, no setting is changed and your config is not written. An install already on the front-door surface, or one withsurface_offer_seenset, is told nothing and gets no marker. Silence it the same way:jcodemunch-mcp config set surface_offer_seen true. -
A one-time notice that an installed policy can be refreshed.
initwrites an agent policy block into~/.claude/CLAUDE.md(orCLAUDE.mdin the directory it was run from) once and never rewrites it. When the server answers its first successfulresolve_repocall, it reads~/.claude/CLAUDE.mdandCLAUDE.mdin the server process's working directory (not the repository the call names) and, if the block in either differs from the one this install would write, adds apolicy_noticeobject to that answer: the file's path, how many lines differ, the commandjcodemunch-mcp claude-md --refresh, and a line asking the agent to tell you once and not to run the command itself. A marker at~/.code-index/policy_notice_state.jsonrecords that it was said (a timestamp, the tool, the version, a digest of the policy text, and the settings that policy was chosen from: the tool surface and, under the full surface, the names of the tools the server offered; nothing else). It is said again only when the policy this install would write changes: a new version's text, or a config change that selects a different policy (tool_surface,tool_profile,disabled_tools). It can also be said again if the marker cannot be written. When the blocks are current, or there is no block, nothing is said and no marker is written. There is no separate switch for it: it is said at most once per policy text, and refreshing the block or leaving it as it is are both your choice. Nothing is sent anywhere and no file is changed by the server. The command itself writes only when you run it: it prints the diff, asks before writing (or takes--yes), copies the file to a newCLAUDE.md.jcodemunch-<time>.bakbeside it (beside the link's target, whenCLAUDE.mdis a symlink), and replaces only the policy block: from its## Code Exploration Policyheading to the next heading of any level that is not one of the policy's own sections. Text you added directly under the block with no heading of its own counts as part of the block, and the diff shows it before anything is written.--dry-runshows the diff and writes nothing. The server chooses its policy from the settings in the MCP client's environment and the command from your shell's, so the two can disagree (an environment variable such asJCODEMUNCH_TOOL_SURFACEset only for the client). The command says so when they do, and--match-serverwrites this version's policy for the settings recorded in that marker instead of the shell's, with the same diff, question and backup. The marker holds settings, never text: the command builds the block from its own copy of the policy, so an edited marker can only choose which of jcodemunch's policies is written. A marker whose settings it cannot read writes nothing, and the version and time it prints from the marker are printed only in the shape the server writes them. -
A one-time license notice, for installs that never run
init.initends with a license line, but auvxor plugin install starts the server directly and never shows it. So the first successful tool answer on a machine carries one plainlicense_noticefield: "License: free for personal and other non-commercial use. Work use needs a commercial license, paid once, that includes every 1.x update: https://jcodemunch.com/buying.php". It is a statement, not a request to the agent. The same line is written once to the server's stderr. It is added once per machine: a marker at~/.code-index/license_notice_state.json(underCODE_INDEX_PATHif you set it) records that it was said (a timestamp, what said it, a tool orinit, and the server version, nothing else), and aninitthat prints its own license line writes the same marker (upgradere-runsinitwithout printing it and writes nothing). It can be said a second time if the marker cannot be written, or if two servers answer their first call at the same moment. It is never added when a license key is configured (read locally, never sent), after aninitthat printed the license line (aninitfrom before this release left no marker, so that machine sees the line once), withJCODEMUNCH_LICENSE_NOTICE=0, or on a CI or remote runner (CI=trueorCLAUDE_CODE_REMOTE=true). Nothing is sent anywhere and no setting is changed. -
Two local per-tool counters, and the
diagnosereport. For each tool the server keeps two counts in~/.code-index/_tool_counters.json: calls, and tokens returned (the answer's size in bytes divided by 4; the file names that estimator). The file holds tool names, the two counts, and the date counting began. It holds no query, path, repository name or file content. The server adds only names from its own catalog, so a name a client makes up is never written; a name already in the file is carried forward as it stands. A call relayed by the front door is counted under both names (orderand the tool it ran) and its size under one, the tool that answered. Counting never waits for the disk: the file is written by a short-lived thread every 20 calls, or on the first call after 30 seconds, and when the server stops, so a server that is killed loses the counts it had not written. A write takes a lock file beside it,_tool_counters.lock, for a few milliseconds, so that several servers do not overwrite each other; a server that stops while another holds the lock, or just after one was killed holding it, can take up to 3 seconds longer to exit. Nothing is sent anywhere: the savings telemetry payload is unchanged and does not read this file. Turn it off with"tool_counters": falseinconfig.jsonc; nothing is then recorded and no file is written.jcodemunch-mcp diagnosereads these counts, plus versions, index sizes, the tool surface and the state of each installed policy, and writes one report to~/.code-index/diagnose/each time it is run. The report is built from a fixed list of fields and of what each value may be: an index appears asindex-1, never by name or path, a tool profile you named yourself appears ascustom, and theanon_idis left out unless you pass--include-anon-id.--stdoutprints the report and writes no file. Nothing is sent; a report stays on your machine until you delete it or share it yourself, and old reports are not pruned.uninstalldoes not yet remove these files. -
Local per-tool savings attribution.
~/.code-index/_savings.jsonrecords how much of your lifetime token-savings total each tool earned (a map of tool name to a running count, plus the date attribution started). It exists because the meter previously stored a single lifetime number, so "which tools produced this" had no answer — including for us, when we needed to size the effect of a baseline correction. This stays on your machine: the anonymous meter above sends the aggregate total and never a per-tool breakdown, and a test in the suite fails if that payload ever changes shape. Tool names only — no code, no paths, no queries, no repo names. Counts earned before this shipped cannot be attributed retroactively and are reported aslifetime_unattributedrather than dropped. Read it withget_session_stats; delete~/.code-index/to erase it. -
Startup check for an index of the current directory (stdio only). When the server starts on the stdio transport, one thread runs the same lookup as the
resolve_repotool to ask whether the working directory is already indexed. If it is, and the index was built from this directory, the server's startup note says so and gives the file count. The lookup makes no network call. Like any firstresolve_repocall, it may create~/.code-index/if it doesn't exist, convert an old JSON-format index there to the current format, and run localgitcommands (such asgit rev-parse) to identify the repository. Startup waits at most 1.5 seconds for it; after that the note keeps its default wording and the thread is left to finish. The SSE and streamable-http transports don't run it. -
Startup import of the local embedding backend. When a native embedding provider is configured (the bundled ONNX encoder, or a sentence-transformers model), the server imports that library at startup, on the main thread, before it begins serving. That adds a few seconds to launch and is not optional polish: importing it later, on the worker thread a tool call runs in, deadlocks on the Windows loader lock and hangs the call forever. Nothing is downloaded and no model is loaded — the import alone is what matters, and no network is touched. Opt out with
JCODEMUNCH_EAGER_EMBED_IMPORT=0. -
In-process embedding cache. After a semantic search reads a repository's stored vectors out of
~/.code-index/, the decoded matrix stays in that server process's memory so the next query doesn't re-read and re-decode the whole thing (roughly 46 MB for a 30,000-symbol index). At most 2 repositories are held at a time, it is dropped when the index is written, and it dies with the process — nothing is written anywhere and no network is touched. Turn retention off withJCODEMUNCH_EMBED_MATRIX_CACHE=0. -
Agent hooks.
init/installcan write hook entries (auto-reindex on edit, read-interception nudges) into your MCP client's settings. They're offered during the interactive flow, shown before writing, and fully removed byuninstall. -
A Grep answered from the index (with
init --guidanceor--strict, andJCODEMUNCH_HOOK_GREP_ANSWER=1; off by default). When the PreToolUse hook sees a Grep inside an indexed repo, it runssearch_textin the hook's own short-lived process, within a 4-second budget (JCODEMUNCH_HOOK_ANSWER_BUDGET_SECONDS), and refuses the Grep with that answer as the reason; the same Grep sent again goes through. It keeps~/.code-index/_grep_answered.json: hashes of the session id and the Grep's arguments, the last 200, nothing else. The hook process exits when its budget ends; agitcommand the search started can run on until its own timeout (at most 10 seconds). Nothing touches the network. Secret redaction applies to the answer. -
Text-search filter file. Beside each index,
~/.code-index/<repo>.textfilterholds one trigram bitmap per cached file with that file's size and mtime: hashes, not text. Indexing writes it;search_textrepairs the rows of files it reads without a current one, during the call that reads them. It lets a substring search skip files that cannot match. It is deleted with the index; deleting it by hand only makes searches read every file again. -
Symbol source sent to the AI summarizer — off by default. With
summarizer_body_charsabove0inconfig.jsoncor a project.jcodemunch.jsonc, indexing andsummarize_reposend each symbol's decorators and its own source (cut to that many characters, after secret redaction byredact.redact_text) to the configured summarizer provider, where the default sends signatures only. The text is read from the index's own copy of the file, not from disk. A remote provider (the default Anthropic endpoint, Gemini, a remote OpenAI-compatible base) receives it only whenallow_remote_summarizeris also true; a local endpoint needs no second setting. Redaction is pattern-based and can miss a credential it does not recognise. -
Local index storage. Indexes live at
~/.code-index/(override withCODE_INDEX_PATH). Delete the directory and every trace of indexing is gone.list_reposshows a session only the indexes of its own workspace by default and says how many others it hid;"repo_visibility": "all"lists them all again. It is a listing scope, not an access control. -
Live session journal. While the server runs, it periodically writes a small
_session_live.jsonin~/.code-index/recording the files and searches the agent touched this session (paths and query strings only, no file contents). It exists so the out-of-process PreCompact hook can restore session orientation after context compaction. Throttled, atomically written, overwritten in place; disable withJCODEMUNCH_LIVE_JOURNAL=0. -
Process presence file. While a server runs, it writes one small JSON file at
~/.code-index/_processes/<pid>.jsonrecording its own PID, transport, version, start time, and the launching client's name — nothing about your code, repos, or queries. It exists soget_session_statscan tell you how many jCodeMunch servers are sharing one index store: some MCP clients don't reap stdio servers at session end, and they accumulate invisibly (one user found 25+ holding ~17 GB between them). The file is removed on exit, and any reader prunes entries whose process is no longer alive, so a hard kill leaves nothing behind. No daemon, no timer, no network. -
Transcript root registry. The server (at startup) and the hooks (when one fires) append the directory Claude Code writes this session's transcripts to —
$CLAUDE_CONFIG_DIR/projects, or the grandparent of the hook payload'stranscript_path— to a small JSON list at~/.code-index/_transcript_roots.json. Directory paths only: no transcript contents, no repo paths, no queries, no file contents, and nothing about your code. It exists sojcodemunch-mcp receiptcan count the sessions you actually ran: it used to scan a hardcoded~/.claude/projects, so anyone running a secondCLAUDE_CONFIG_DIRprofile saw a ledger missing most of their calls (one user measured 12 of 348). No daemon, no timer, no network.receipt --rootsprints exactly what is registered; delete the file to clear it, and delete~/.code-index/to erase everything. -
Local performance ledger — off by default. With
perf_telemetry_enabled: trueinconfig.jsonc, the server records tool latencies, ranking events (query strings and returned symbol ids), and one per-session row of delivery counts (session_yield: how many symbols were delivered, how many of those were the same symbol twice, and an estimated token count for the repeats) into~/.code-index/telemetry.db. It is whatanalyze_perf,suggest_corrections, and the weight tuner read. This database never leaves your machine — the anonymous savings meter above sends aggregate counters only and never reads this file. Off unless you turn it on; delete the file to erase it. -
Git history read during indexing — off by default. With
git_blame_enabled: trueinconfig.jsonc, indexing a git working tree runs one boundedgit log(at most 20,000 commits, two years back from the latest commit, 10 s) and stores each covered file's last commit author name and author date in the index under~/.code-index/. Nothing leaves the machine unless AI summaries are on, in which case that name and date go to your configured summarizer as context. It is not used for search ranking or file summaries. Releases up to 1.108.364 ran it by default, and an index they built keeps those names and dates for every file that hasn't changed since; re-index withincremental: falseto drop them. -
User-invoked network calls. A few commands you run explicitly reach the network. None run in the background or fire on a plain import; each happens only when you invoke the command:
- License validation.
license,org-rollup, andinstall-pack --licensesend your license key tovalidate.phponj.gravelle.usto confirm it. The key travels in the request body / a header, never the URL, so it can't land in intermediary access logs. This gates only the teamorg-rollupfeature; the individual tools never call it. - Starter-pack download.
install-packfetches the pack catalog and any pre-built index pack you request fromjcodemunch.com(a premium pack also sends your license key). After a successful install it also posts one event tojcodemunch.comnaming the pack id, its version and your platform (no license key, no paths, no code). It is skipped when savings sharing is off, the same setting the savings counter reads:share_savings: falseinconfig.jsonc, orJCODEMUNCH_SHARE_SAVINGSset tofalse,nooroffwhenconfig.jsoncdoes not setshare_savings(an explicit value in the file takes precedence over the env var).JCODEMUNCH_SHARE_SAVINGS=0always skips it. Each pack indexes third-party open-source repositories and carries their license and attribution files verbatim underlicenses/<owner>-<name>/in your index directory;install-packprints the terms and the path, andinstall-pack --listnames them before you download. - Embedding-model download.
download-model— and the first semantic encode when the[local-embed]extra is installed — downloads the ONNX model (all-MiniLM-L6-v2, about 90 MB, one time) fromhuggingface.co, pinned to one upstream revision. Each fetched file is checked against its SHA-256 before it replaces anything on disk, anddownload-modelre-checks a copy already on disk and fetches it again if it does not match; after that, semantic search needs no network. - Team savings report.
org-report(team SKU) sends onlyorg_id,seat_id,tokens_saved,usd,calls, and a date. No code, no file paths, no queries, no repo names. It goes to a host you choose, on your own network, never to a jMunch server. With no--endpointorJCODEMUNCH_ORG_ENDPOINTset it writes to a local file (org_savings.db) and nothing leaves the machine at all. ⚠seat_iddefaults to your machine's hostname, which often contains a person's name: setJCODEMUNCH_CLIENT_IDto send an identifier of your choosing instead. It runs only when you invoke it. There is no scheduler and no background reporting.
- License validation.
-
Grammar download on an overridden dependency pin — never by default. The shipped
tree-sitter-language-packpin is<1.0.0, whose wheels bundle every grammar. F# comes from the pinnedtree-sitter-fsharpwheel instead, compiled in the same way, so it never downloads (#848). If you override it to 1.x (pip install -U tree-sitter-language-pack), that pack fetches each grammar over the network into its own cache directory at first parse, the nim grammar there is one our extractor cannot read, and the server reports it: agrammar_packblock and warning on everyindex_folderresult, and aGrammar packsection ininstall-status. Documented in README under Security (#608, #382). -
Accepting reports from other machines. Off by default, behind explicit gates. jCodeMunch has four routes that accept writes from another computer. All four are off in a default install, all four require the HTTP transport to be running at all (
serve --transport streamable-httpor--transport sse), and all four require the request to carry yourJCODEMUNCH_HTTP_TOKENbearer.POST /org/report— one machine acting as the "org host" that collects the seat reports above. Gated a third way byorg_ingest_enabled, which defaults to false (JCODEMUNCH_ORG_INGEST_ENABLED=1). It stores exactly the six fields listed above, inorg_savings.dbon that host.POST /runtime/otel,POST /runtime/sql,POST /runtime/stack— live ingestion of runtime traces, the same dataimport-traceaccepts from a file. Gated a third way byruntime_ingest_enabled, which defaults to false (JCODEMUNCH_RUNTIME_INGEST_ENABLED=1). Bodies are capped (JCODEMUNCH_RUNTIME_INGEST_MAX_BODY_BYTES, 5 MB default, checked before and after decompression as a gzip-bomb guard) and PII is redacted at the ingest chokepoint unless you turn that off.
With the token unset these routes return 503 rather than running unauthenticated — a missing token disables the endpoint instead of opening it. A default install accepts nothing, listens for nothing, and needs no action from you to keep it that way.
⚠ Until v1.108.274 this list named
POST /org/reportand described it as the sole remote-write route. That was true when written and was not revisited when the runtime-ingest routes were added. Reported by @elfrost (#449). The count is now pinned bytests/test_security_disclosure.py, because the promise this section makes is that the enumeration is complete, and a sentence cannot keep that promise on its own.
Beyond the user-invoked calls listed above, the base package makes no other network calls and leaves no other persistent processes. AI-summary extras call their configured provider's API only when you enable them — see the extras matrix below.
Most extras are pure-Python and self-contained. A few pull libraries that touch system surfaces worth noting for managed-endpoint and SOC 2 / HIPAA-adjacent deployments. For the base package alone, none of these surfaces are introduced.
| Extra | Transitive dependencies of note | System surfaces |
|---|---|---|
| (base, no extra) | none | none |
[local-embed] |
onnxruntime |
local CPU inference (no network after model download); model fetched on first run |
[anthropic] |
anthropic SDK |
outbound HTTPS to api.anthropic.com when AI summaries are enabled |
[gemini] |
google-generativeai |
outbound HTTPS to Google AI endpoints when AI summaries are enabled |
[openai] |
openai SDK |
outbound HTTPS to api.openai.com (or OPENAI_API_BASE) when AI summaries are enabled |
[groq] |
openai SDK |
outbound HTTPS to Groq endpoints; used by the gcm CLI and speedreview Action |
[groq-voice] |
sounddevice, numpy |
microphone access — sounddevice.InputStream opens the system audio device when the voice path is invoked |
[groq-explain] |
Pillow (Python 3.11+ only; not installed on 3.10, where no patched Pillow exists) |
image decode / re-encode of attached screenshots |
[all] |
union of all the above | union of all surfaces above, including microphone ([groq-voice]) and image libraries ([groq-explain]) |
For managed-endpoint deployments where microphone access on developer machines
is policy-restricted (HIPAA, SOC 2, finance), pin to the base package or to the
specific provider extras you need. The voice and explain paths are opt-in
features, not part of the core MCP server functionality, and [all] is the
only extra that bundles them together.