downscoping-mcp
Least-privilege credential injection for Claude Code. Intercepts Bash and MCP tool calls before they execute, swapping in the minimum-privilege token required for each operation based on a declarative YAML policy.
Problem
Claude Code runs with whatever credentials are present in your environment. A model that can read files can also call gh repo delete, gcloud projects delete, or aws iam delete-user — using the same token. A single jailbreak, prompt injection, or confused-deputy attack is enough to cause damage.
Solution
Two complementary enforcement modes:
Mode 1 — Bash hook (CLI tools)
A PreToolUse hook intercepts every Bash tool call. If the command starts with a known service binary (gh, gcloud, aws, kubectl), the hook pattern-matches the arguments against your policy rules, selects the appropriate token slot, and rewrites the command to prepend the scoped credential before execution. Claude never sees the rewrite.
Mode 2 — MCP proxy An MCP proxy server wraps an upstream MCP server. Before forwarding each tool call, it injects the scoped token for that specific tool into the subprocess environment. Read-only tools get a read-only token; write tools require an elevated token.
Quick Start
1. Install
pip install -e .
2. Configure credentials
Export scoped tokens in your shell profile or CI environment:
export GITHUB_TOKEN_READONLY=ghp_... # fine-grained: contents:read, issues:read
export GITHUB_TOKEN_ORG_WRITE=ghp_... # fine-grained: issues:write, pull_requests:write
export GCLOUD_TOKEN_VIEWER=ya29....
export GCLOUD_TOKEN_EDITOR=ya29....
3. Create a policy file
Copy config.example.yaml to .claude/downscoping.yaml in your project root:
cp config.example.yaml .claude/downscoping.yaml
Edit the rules to match your org's access model. The file is .gitignore-safe for personal token slot names; the structure itself can be committed.
4. Register the hook
Add to your project's .claude/settings.json:
{
"env": {
"CLAUDE_PLUGIN_ROOT": "/path/to/downscoping-mcp"
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py",
"timeout": 5
}
]
}
]
}
}
5. (Optional) Enable the MCP proxy
Add to .mcp.json in your project root:
{
"mcpServers": {
"credential-downscope-proxy": {
"command": "python3",
"args": ["-m", "credential_downscope.mcp_proxy"],
"env": {
"PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/src",
"GITHUB_INTEGRATION_SRC": "/path/to/upstream-mcp-server/src"
}
}
}
}
Policy File Reference
version: 1
services:
gh:
token_slots:
readonly:
env_var: GITHUB_TOKEN_READONLY # read from process env
inject_as: GITHUB_TOKEN # injected into subprocess
org-write:
env_var: GITHUB_TOKEN_ORG_WRITE
inject_as: GITHUB_TOKEN
default_slot: readonly # used when no rule matches
rules:
- name: "writes need elevated token"
match:
args_pattern: "pr (create|merge|edit)|issue (create|edit)|push"
slot: org-write
Token resolution order:
- Read
env_varfrom the current process environment - If unset, fall back to the
inject_asvariable (uses the ambient credential) - If neither is set, pass the command through unmodified
Architecture
Claude Code
│
├─ Bash tool call ──► PreToolUse hook (hooks/pre_tool_use.py)
│ │
│ ├─ load .claude/downscoping.yaml
│ ├─ detect service binary
│ ├─ match args against rules
│ ├─ resolve token slot
│ └─ rewrite command: TOKEN=value <original command>
│
└─ MCP tool call ──► credential-downscope-proxy (mcp_proxy.py)
│
├─ receive tool call from Claude
├─ match tool name against MCP rules
├─ inject scoped token into env
└─ forward to upstream MCP server
Supported Services
| Service | Binary | Token env var pattern |
|---|---|---|
| GitHub CLI | gh | GITHUB_TOKEN |
| Google Cloud | gcloud | CLOUDSDK_AUTH_ACCESS_TOKEN |
| AWS CLI | aws | AWS_ACCESS_KEY_ID |
| Kubernetes | kubectl | KUBE_TOKEN |
| MCP servers | via proxy | configurable per server |
Additional services can be added by extending config.yaml — no code changes required.
Security Notes
- Token values are
shlex.quote-escaped before shell injection to prevent command injection via crafted token values. - Prepending
TOKEN=valuebefore a command makes the token visible in process listings (ps aux). For higher-security environments, use a credential helper that injects tokens via a file descriptor or secrets manager. - The hook never blocks commands (exit 0 always); it only rewrites them. Blocking logic can be added by returning
{"continue": false}from the hook. .claude/settings.jsoncontaining local paths should be gitignored — see.gitignorein this repo for an example.
Development
pip install -e ".[dev]"
pytest tests/
License
MIT