Skip to content

Latest commit

 

History

History
535 lines (424 loc) · 52.7 KB

SECURITY.md

File metadata and controls

535 lines (424 loc) · 52.7 KB

Security Controls

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.


Reporting a vulnerability

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.md and the release notes, unless you ask otherwise.
  • Scope: the jcodemunch-mcp package 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.

Past advisories (all fixed)

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.


Path Traversal Prevention

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 of root using os.path.commonpath().
  • Applied during file discovery and again before each file read (defense in depth).
  • Paths such as ../../etc/passwd or absolute paths outside the repository root are rejected.

Symlink Escape Protection

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.

Default Ignore Policy

Files are filtered through multiple layers:

  1. SKIP_PATTERNS — directories and files always excluded (e.g., node_modules/, vendor/, .git/, build/, dist/, generated files, lock files).
  2. .gitignore — respected by default for both local folders and GitHub repositories (via the pathspec library).
  3. extra_ignore_patterns — user-configurable additional gitignore-style patterns passed to indexing tools.

Secret Exclusion

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/*.der are not excluded just for living here).
  • Secret-store data: data/config files (*.yaml, *.json, *.tfvars, *.tfstate[.backup], …) under a whole-segment secret/secrets/credential/ credentials/creds/vault directory.
  • Broad secret basename: a secret/secrets token at a word boundary (so prod-secrets.yaml is excluded but secretariat.csv is not), excluding source-code and documentation files (secret_redaction.py is 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.


Response-Level Secret Redaction

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.

Three tools are exempt, deliberately

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.

File Size Limits

  • Default maximum: 500 KB per file (configurable via max_file_size in config, the JCODEMUNCH_MAX_FILE_SIZE environment variable, or the max_size argument on a single index_folder / index_repo call).
  • 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 reports complete: false and 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_FILES environment variable.

Cache Directories

  • jCodeMunch honours the Cache Directory Tagging Specification. A directory containing a CACHEDIR.TAG file whose first 43 bytes are Signature: 8a477f597d28d172789f06886806bc55 is pruned from the walk along with everything beneath it.
  • The signature is verified. A file merely named CACHEDIR.TAG excludes 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_dir in discovery_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: false or JCODEMUNCH_RESPECT_CACHEDIR_TAG=0 if 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.

Python Virtualenvs

  • A directory whose pyvenv.cfg carries a home = key (PEP 405, written by every venv tool) is a virtualenv root, whatever it is called. Its installed packages, the site-packages directory at Lib/site-packages or lib/pythonX.Y/site-packages, are pruned from the walk.
  • Only site-packages is 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 own bin/ and lib/.
  • A file merely named pyvenv.cfg with no home key prunes nothing.
  • Pruned directories are counted as virtualenv in discovery_skip_counts. This is an ordinary exclusion, not a withholding, so absence claims over the remaining corpus stay citable.
  • Disable with skip_virtualenvs: false or JCODEMUNCH_SKIP_VIRTUALENVS=0. Applies to local folder indexing only.

Binary File Detection

Binary files are excluded using a two-stage check:

  1. Extension-based detection — common binary extensions (.exe, .dll, .so, .png, .jpg, .zip, .wasm, .pyc, .class, .pdf, .db, .sqlite, etc.).
  2. 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.

Encoding Safety

  • 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 Never Runs Indexed Code Itself

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.


Storage Safety

  • Index storage defaults to ~/.code-index/.
  • The storage path can be overridden using the CODE_INDEX_PATH environment 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.

Release artifact signing

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.


Files this server treats as security-sensitive

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 that jcodemunch-mcp init writes 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-guidance strips the jcodemunch block from each, backing the file up first.
  • ~/.claude/settings.json (PreToolUse / PostToolUse / PreCompact / SessionStart / TaskCompleted / SubagentStart / WorktreeCreate / WorktreeRemove hooks) — a default init registers 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 (--guidance or --strict) and JCODEMUNCH_HOOK_GREP_ANSWER=1 (off by default), the PreToolUse hook answers a Grep inside an indexed repo from the index: it runs search_text locally (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, and search_text repairs 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.json written by claude 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.


Persistent processes installed by watch-install

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 with systemctl --user enable --now jcodemunch-watch.service.
  • macOS (launchd LaunchAgent): ~/Library/LaunchAgents/us.gravelle.jcodemunch-watch.plist. Loaded with launchctl bootstrap gui/$UID <plist>.
  • Windows (Task Scheduler entry): task named jcodemunch-watch under 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/.


Cache integrity verification modes

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."


Telemetry Data Locality

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 when perf_telemetry_enabled: true (or JCODEMUNCH_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 explicit tune_weights invocation.
  • ~/.code-index/embed_canary.json (16-string drift canary) is written only by an explicit check_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 with JCODEMUNCH_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.

Headless automation on issues and pull requests

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.md section 9) confines to inbound/** branches and the inbound-ledger branch.
  • 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-ledger branch. The switch is off by default and fails closed.

Summary of Controls

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

Background behavior, fully disclosed

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-claude commands (and watch: true in 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 .git pointer file and commondir where the folder is a linked worktree or a submodule (at most 4096 bytes of each), and HEAD and the one ref file HEAD names (at most 256 bytes of each), and it takes the size and modification time of packed-refs and reftable/tables.list. When the commit has moved with no file event it runs git rev-parse HEAD and 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-install registers watch-all as a login service (Windows Task Scheduler / macOS launchd / Linux systemd) so indexes stay fresh across reboots. This happens only when you run watch-install yourself; 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 run watch-install again. Inspect it with watch-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 with watch-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 at server/savings/post.php: for the integer lifetime total this client sends, it keeps the larger of that and the stored total (a total outside its accepted range is refused and stores nothing), and it adds delta only when a request carries no usable total, 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 with share_savings: false in config.jsonc or JCODEMUNCH_SHARE_SAVINGS=0; redirect the endpoint with JCODEMUNCH_TELEMETRY_URL.

  • One-time tool-surface notice. On server start, if your tool_surface is 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.json records 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 because tool_surface is 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 with jcodemunch-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_repo answer an install makes therefore carries the same offer as a surface_notice object: 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.json records 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 with surface_offer_seen set, 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. init writes an agent policy block into ~/.claude/CLAUDE.md (or CLAUDE.md in the directory it was run from) once and never rewrites it. When the server answers its first successful resolve_repo call, it reads ~/.claude/CLAUDE.md and CLAUDE.md in 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 a policy_notice object to that answer: the file's path, how many lines differ, the command jcodemunch-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.json records 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 new CLAUDE.md.jcodemunch-<time>.bak beside it (beside the link's target, when CLAUDE.md is a symlink), and replaces only the policy block: from its ## Code Exploration Policy heading 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-run shows 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 as JCODEMUNCH_TOOL_SURFACE set only for the client). The command says so when they do, and --match-server writes 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. init ends with a license line, but a uvx or plugin install starts the server directly and never shows it. So the first successful tool answer on a machine carries one plain license_notice field: "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 (under CODE_INDEX_PATH if you set it) records that it was said (a timestamp, what said it, a tool or init, and the server version, nothing else), and an init that prints its own license line writes the same marker (upgrade re-runs init without 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 an init that printed the license line (an init from before this release left no marker, so that machine sees the line once), with JCODEMUNCH_LICENSE_NOTICE=0, or on a CI or remote runner (CI=true or CLAUDE_CODE_REMOTE=true). Nothing is sent anywhere and no setting is changed.

  • Two local per-tool counters, and the diagnose report. 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 (order and 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": false in config.jsonc; nothing is then recorded and no file is written. jcodemunch-mcp diagnose reads 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 as index-1, never by name or path, a tool profile you named yourself appears as custom, and the anon_id is left out unless you pass --include-anon-id. --stdout prints 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. uninstall does not yet remove these files.

  • Local per-tool savings attribution. ~/.code-index/_savings.json records 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 as lifetime_unattributed rather than dropped. Read it with get_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_repo tool 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 first resolve_repo call, it may create ~/.code-index/ if it doesn't exist, convert an old JSON-format index there to the current format, and run local git commands (such as git 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 with JCODEMUNCH_EMBED_MATRIX_CACHE=0.

  • Agent hooks. init / install can 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 by uninstall.

  • A Grep answered from the index (with init --guidance or --strict, and JCODEMUNCH_HOOK_GREP_ANSWER=1; off by default). When the PreToolUse hook sees a Grep inside an indexed repo, it runs search_text in 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; a git command 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>.textfilter holds one trigram bitmap per cached file with that file's size and mtime: hashes, not text. Indexing writes it; search_text repairs 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_chars above 0 in config.jsonc or a project .jcodemunch.jsonc, indexing and summarize_repo send each symbol's decorators and its own source (cut to that many characters, after secret redaction by redact.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 when allow_remote_summarizer is 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 with CODE_INDEX_PATH). Delete the directory and every trace of indexing is gone. list_repos shows 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.json in ~/.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 with JCODEMUNCH_LIVE_JOURNAL=0.

  • Process presence file. While a server runs, it writes one small JSON file at ~/.code-index/_processes/<pid>.json recording its own PID, transport, version, start time, and the launching client's name — nothing about your code, repos, or queries. It exists so get_session_stats can 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's transcript_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 so jcodemunch-mcp receipt can count the sessions you actually ran: it used to scan a hardcoded ~/.claude/projects, so anyone running a second CLAUDE_CONFIG_DIR profile saw a ledger missing most of their calls (one user measured 12 of 348). No daemon, no timer, no network. receipt --roots prints 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: true in config.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 what analyze_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: true in config.jsonc, indexing a git working tree runs one bounded git 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 with incremental: false to 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, and install-pack --license send your license key to validate.php on j.gravelle.us to 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 team org-rollup feature; the individual tools never call it.
    • Starter-pack download. install-pack fetches the pack catalog and any pre-built index pack you request from jcodemunch.com (a premium pack also sends your license key). After a successful install it also posts one event to jcodemunch.com naming 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: false in config.jsonc, or JCODEMUNCH_SHARE_SAVINGS set to false, no or off when config.jsonc does not set share_savings (an explicit value in the file takes precedence over the env var). JCODEMUNCH_SHARE_SAVINGS=0 always skips it. Each pack indexes third-party open-source repositories and carries their license and attribution files verbatim under licenses/<owner>-<name>/ in your index directory; install-pack prints the terms and the path, and install-pack --list names 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) from huggingface.co, pinned to one upstream revision. Each fetched file is checked against its SHA-256 before it replaces anything on disk, and download-model re-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 only org_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 --endpoint or JCODEMUNCH_ORG_ENDPOINT set it writes to a local file (org_savings.db) and nothing leaves the machine at all. ⚠ seat_id defaults to your machine's hostname, which often contains a person's name: set JCODEMUNCH_CLIENT_ID to send an identifier of your choosing instead. It runs only when you invoke it. There is no scheduler and no background reporting.
  • Grammar download on an overridden dependency pin — never by default. The shipped tree-sitter-language-pack pin is <1.0.0, whose wheels bundle every grammar. F# comes from the pinned tree-sitter-fsharp wheel 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: a grammar_pack block and warning on every index_folder result, and a Grammar pack section in install-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-http or --transport sse), and all four require the request to carry your JCODEMUNCH_HTTP_TOKEN bearer.

    • POST /org/report — one machine acting as the "org host" that collects the seat reports above. Gated a third way by org_ingest_enabled, which defaults to false (JCODEMUNCH_ORG_INGEST_ENABLED=1). It stores exactly the six fields listed above, in org_savings.db on that host.
    • POST /runtime/otel, POST /runtime/sql, POST /runtime/stack — live ingestion of runtime traces, the same data import-trace accepts from a file. Gated a third way by runtime_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/report and 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 by tests/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.


Optional extras — system surfaces each pulls in

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.