What a cloned repo's settings file can still run
Claude Code just stopped repositories from switching on telemetry export. The narrower fix hides a wider fact: a committed settings file is code that runs as you, and in a headless run nothing asks first. Interactively a dialog does ask, and I accept it without reading.
Claude Code v2.1.282 shipped on September 24 with a line in the release notes that is easy to scroll past: project and local settings now ignore the OpenTelemetry variables "that turn on export, set its endpoint, or capture content." Read it backwards and it says something less comfortable. Until that release, a .claude/settings.json committed to a repository could switch on telemetry, pick the collector, and ask for prompt and tool content to be included. Clone the repo, start a session, and your session data could go wherever the repository's author pointed it.
That door is closed now. The rest of the file is still open.
What did v2.1.282 change?
It made project and local settings drop the telemetry variables that enable export, route it, or add content to it, and it added a startup notice plus /status and claude doctor entries that name the variables it dropped. A repository can still turn telemetry off: none for the three exporter selectors, and an off value such as 0 for the prompt, tool content and tool detail logging switches.
The full list in the settings reference covers CLAUDE_CODE_ENABLE_TELEMETRY, the exporter selectors, the OTEL_LOG_* content switches, and the OTEL_EXPORTER_OTLP_* endpoint, header, protocol and certificate variables. It sits next to an older group the same file already could not set: CLAUDE_CONFIG_DIR, HOME, TMPDIR and the XDG_* family, which choose where Claude Code writes its own files, and OTEL_LOG_RAW_API_BODIES. The docs date most of that earlier group to v2.1.251. So v2.1.282 is one more step in a steady narrowing of what a repository gets to decide for you.
I ran it on 2.1.283 in a throwaway repo, with a project file that tried to set two telemetry variables, and claude doctor reported:
Claude Code ignores these telemetry variables in .claude/settings.json:
CLAUDE_CODE_ENABLE_TELEMETRY, OTEL_EXPORTER_OTLP_ENDPOINT.
A project's settings files can only turn telemetry offThe pre-fix behavior is documented here, not reproduced: the "before" half rests on the docs and the release notes.
If a team relied on its committed settings to route its own telemetry, that stopped on upgrade. An interactive session shows the startup notice; a -p run or an SDK session shows nothing, so the docs advise checking that the collector still receives data. The variables belong in user settings, managed settings, the job's environment, or a file passed with --settings. None of those is read from the repository's own settings files, which is the point of the change; a --settings file only helps if it lives outside the repo.
What can a cloned repo's settings still do?
A lot, and most of it executes. The permissions docs have a table titled "What runs before you trust a folder," and it is the most useful page Anthropic has written on this subject. Condensed, a repository's committed config splits three ways:
- Applied even in a folder you never trusted (under
claude -p, or when you trusted only a parent folder; parent trust never extends into a nested git repository such as a clone): hooks in settings files, theenvblock minus the variables the settings reference lists as ignored, helper commands such asapiKeyHelperandawsAuthRefresh, and a project skill's hooks andallowed-tools. For skills the docs add that workspace trust never gatesallowed-toolsin any session. - Held until you accept the trust dialog for that folder:
permissions.allowrules andadditionalDirectories, which grant capability, and aheadersHelperon an MCP server (since v2.1.238), for which an interactive session asks again while the server connects with its static headers alone.denyandaskrules apply at once, because they only restrict. - Not used without trust of that folder, in any session type: frontmatter hooks in a project subagent (since v2.1.218), inline
mcpServersin subagent frontmatter (since v2.1.238), a project@skills-dirplugin, andextraKnownMarketplaces. These are skipped rather than prompted for.
Servers in .mcp.json are their own case. Interactively, Claude Code asks before connecting them, and since v2.1.196 a repository cannot approve its own servers: enableAllProjectMcpServers or enabledMcpjsonServers committed to the project file is ignored in an untrusted folder. Under claude -p they connect without asking, approved or not.
Look at the version numbers down that list: 2.1.196, 2.1.218, 2.1.238, 2.1.251, 2.1.282. Each one took one kind of repository content out of the repository's hands or pushed it behind the trust line. This is hardening surface by surface, not one principle applied once, and it means the version you run matters. A pinned old CLI in a CI image still has the gaps the later releases closed.
Does the trust dialog protect a claude -p run?
No. The docs say it outright: a -p run or an SDK session never shows the dialog, and for hooks and env it counts as accepted. In a headless run the repository's hooks and environment apply in a folder you have never opened.
Here is the repro. A repository with this committed as .claude/settings.json:
{
"env": {
"PROBE_VAR": "set-by-repo",
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:9"
},
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command",
"command": "env | grep -E '^(PROBE_VAR|CLAUDE_CODE_ENABLE|OTEL_EXPORTER)' > /tmp/hook-ran.txt" } ] }
]
}
}Three runs on Claude Code 2.1.283, in a folder with no trust entry in ~/.claude.json:
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 < /dev/null
cat /tmp/hook-ran.txt
# PROBE_VAR=set-by-repo
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 \
--settings '{"disableAllHooks": true}' < /dev/null
# no /tmp/hook-ran.txt
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 \
--setting-sources user < /dev/null
# no /tmp/hook-ran.txtLook at the first run. The repository's hook ran before the model said a word, it saw the repository's PROBE_VAR, and it did not see the two telemetry variables, because v2.1.282 dropped them. The hook here writes a file. A real one could run anything your account can run, with whatever credentials your environment holds, which is the point I made about installed tools having your reach: the authority is ambient, and a hook inherits all of it.
What does an interactive session hold back until you trust the folder?
Everything that runs, until you say yes. The hooks docs say an interactive session holds back hooks from every settings file, your own ~/.claude/settings.json included, until you accept the trust dialog for the folder or for a parent whose trust extends to it. The dialog lists the allow rules and additional directories the folder would grant, so you can review them first. After you accept, all of it applies, hooks and helpers included.
So interactively the dialog is the gate, and this is where I have to be honest about my own setup. My agents run as interactive Claude Code sessions in tmux, not as -p scripts. That has been true since before the Agent SDK credit change, and I don't run -p over repositories I didn't write, so the headless gap above is not my exposure. The dialog is. And I don't read it. I have never opened a repository's .claude/settings.json or .mcp.json before accepting trust, and nothing has ever bitten me for it, which proves nothing about the next repo.
A gate you click through by reflex is a gate on paper. The dialog does its job only if the list it shows gets read, and a list of grants in a modal, at the moment you want to start working, is exactly the thing I accept without reading.
How do you check a repo before you trust it?
Read the files the dialog summarizes, before the dialog appears, with a script that pulls out only the keys that run or grant something. This is plain Python with no dependencies. Run it from the repository root:
import json, pathlib, subprocess
KEYS = ["env", "hooks", "apiKeyHelper", "awsAuthRefresh", "awsCredentialExport",
"otelHeadersHelper", "statusLine", "subagentStatusLine", "fileSuggestion",
"enabledPlugins", "extraKnownMarketplaces"]
for name in [".claude/settings.json", ".claude/settings.local.json"]:
p = pathlib.Path(name)
if p.exists():
s = json.loads(p.read_text())
found = {k: s[k] for k in KEYS if k in s}
perms = s.get("permissions", {})
for k in ["allow", "additionalDirectories"]:
if perms.get(k):
found["permissions." + k] = perms[k]
print(name, json.dumps(found, indent=2))
mcp = pathlib.Path(".mcp.json")
if mcp.exists():
for n, srv in json.loads(mcp.read_text()).get("mcpServers", {}).items():
print("mcp server:", n, json.dumps(srv))
for d in [".claude/agents", ".claude/skills", ".claude/commands"]:
if pathlib.Path(d).is_dir():
print("read the files in", d)
r = subprocess.run(["git", "ls-files", ".claude/settings.local.json"],
capture_output=True, text=True)
if r.returncode != 0:
print("git check failed:", r.stderr.strip())
elif r.stdout.strip():
print("settings.local.json is tracked in git: treat it as repository-supplied")On my first test repository, which also carried a one-server .mcp.json, it printed the env block, the SessionStart hook with its full command, and the full npx definition of that server. It is a first pass, not a clean bill of health: it reads only the repository root, and for agents, skills and commands it only tells you which folders to open. Whatever it prints is a list of commands and grants to read before you let them run.
The last check matters more than it looks. A .claude/settings.local.json is normally yours, so once Claude Code has checked it with git, its allow rules skip the trust step. When the file is tracked in git, or .claude is a symlink, Claude Code treats it as repository-supplied and holds its rules like the shared file. A repository that commits a "local" file is telling you something about itself.
This is the same boundary I argued for in instruction files as addressing: text a stranger can commit should never carry the authority of your own standing config. Settings are that boundary with executable teeth.
How do you run claude -p in a repository you didn't write?
Decide what the run may load before it starts. The docs give four options, and they are not interchangeable:
--setting-sources userreads neither the project's settings files nor its.mcp.json. In my repro this stopped the hook. It is the broadest single switch.--bareskips hooks, skills, custom commands, subagents, plugins and.mcp.jsonservers. It does not drop the project'senvblock or helpers such asawsAuthRefresh, and it reads no OAuth login, so on the Anthropic API you needANTHROPIC_API_KEYor anapiKeyHelperpassed through--settings. Skills from a directory you pass with--add-dirstill load. The docs say it will become the default for-pin a future release.--settings '{"disableAllHooks": true}'turns off ordinary user, project and plugin hooks for the run; hooks your organization manages stay on. It has to be on the command line: set in your user settings alone it loses, because project settings outrank user settings and the repository can set it back tofalse. In my repro the flag stopped the hook.disabledMcpjsonServersrejects a named.mcp.jsonserver in every session type, from any settings file.
My read on which one to reach for: --setting-sources user for anything automated over code you didn't write, because it is the only one of the four that removes the project's env and hooks and servers together. --bare is faster and cleaner for CI, but "skips hooks" is not "skips the repository," and the env block is the part --bare leaves in place.
What should you do this week?
Update the CLI anywhere it runs over code you didn't write, including CI images with a pinned version. Add --setting-sources user or --bare to every scripted claude -p over foreign repositories; the docs already say --bare will become the default for -p, which is the same direction as every version number above. Before you accept the next trust dialog, run the audit, or at least open .claude/settings.json and .mcp.json and read the commands in them.
The telemetry variables were the easy case: they only send. The keys that run are still in the file. Between a repository's hooks and your shell there is a dialog in one mode, and in the other, nothing.