repo_mirror_push: remote_password (a secret) is passed as a plain tool arg → lands in transcripts #49

Closed
opened 2026-07-04 06:55:36 +00:00 by mathias · 1 comment
Owner

Problem

repo_mirror_push takes remote_password (a GitHub PAT / mirror credential) as a normal input field. Every MCP tool call is persisted: claude.ai/terminal transcript → ~/.claude/projects → claudewatcher → brain/wiki → gitea history. So calling this tool with a real remote_password writes the secret into searchable, long-lived storage — a leak that requires rotating the credential to remediate.

Surfaced running #19 (Gitea→GitHub mirror E2E): I could not use repo_mirror_push for the mirror-config step without leaking the PAT, so I fell back to the gitea REST API inside op run (PAT confined to the subprocess).

Options

  1. Don't accept the raw secret. Accept an indirection instead — e.g. a remote_password_env naming an env var the server reads at call time, or a 1Password op:// reference the server resolves. The secret never transits the tool-call payload.
  2. Server-side credential store. Look the mirror credential up server-side (keyed by remote host/owner) rather than taking it per-call.
  3. At minimum, document loudly that remote_password must never be a real secret from a logged surface, and that the REST-in-op-run path is the safe route.

Option 1 (env/op:// reference) is the cleanest and matches the platform's secret-handling rule (secrets stay in the subprocess, never in argv/payload).

Acceptance

  • A caller can configure a push-mirror without the credential appearing in the tool-call payload/transcript.
  • Existing raw-password path either removed or gated behind a clear warning.

Refs

Surfaced during #19 (mirror E2E, 2026-07-04). Global secret-handling policy: "Secrets stay in the subprocess; never place a literal secret in a command's argv/payload."

## Problem `repo_mirror_push` takes `remote_password` (a GitHub PAT / mirror credential) as a normal input field. Every MCP tool call is persisted: claude.ai/terminal transcript → `~/.claude/projects` → claudewatcher → brain/wiki → gitea history. So calling this tool with a real `remote_password` **writes the secret into searchable, long-lived storage** — a leak that requires rotating the credential to remediate. Surfaced running #19 (Gitea→GitHub mirror E2E): I could not use `repo_mirror_push` for the mirror-config step without leaking the PAT, so I fell back to the gitea REST API inside `op run` (PAT confined to the subprocess). ## Options 1. **Don't accept the raw secret.** Accept an indirection instead — e.g. a `remote_password_env` naming an env var the server reads at call time, or a 1Password `op://` reference the server resolves. The secret never transits the tool-call payload. 2. **Server-side credential store.** Look the mirror credential up server-side (keyed by remote host/owner) rather than taking it per-call. 3. At minimum, **document loudly** that `remote_password` must never be a real secret from a logged surface, and that the REST-in-`op-run` path is the safe route. Option 1 (env/`op://` reference) is the cleanest and matches the platform's secret-handling rule (secrets stay in the subprocess, never in argv/payload). ## Acceptance - [ ] A caller can configure a push-mirror without the credential appearing in the tool-call payload/transcript. - [ ] Existing raw-password path either removed or gated behind a clear warning. ## Refs Surfaced during #19 (mirror E2E, 2026-07-04). Global secret-handling policy: "Secrets stay in the subprocess; never place a literal secret in a command's argv/payload."
Author
Owner

Fixed in v0.7.1.

Added remote_password_env to repo_mirror_push: the name of a server-side env var the tool resolves at call time (os.Getenv), so the mirror credential never appears in the tool-call payload (→ transcript → claudewatcher → brain → gitea history). An env name that resolves to empty errors loudly (ErrValidation) instead of silently sending an empty password. Raw remote_password still works but the schema now marks it DISCOURAGED and the descriptor steers callers to the env path.

Tests: TestRepoMirrorPushTool_PasswordFromEnv (secret resolved from the env var, never in output) + TestRepoMirrorPushTool_EnvUnsetErrors. task check green (exit 0, 0 FAIL — verified).

Infra follow-up (out of scope for this code change): for remote_password_env to resolve on the live server, the gitea-mcp deployment must actually carry the referenced env var (e.g. a GitHub mirror PAT injected via ESO into the pod env), then callers pass just the var name. Until that secret is wired into the deployment, the raw path or the REST-in-op run workaround remains. Filing/So noting — the code accepts the reference now; provisioning the pod env is the remaining infra step.

Closing #49.

Fixed in v0.7.1. Added **`remote_password_env`** to `repo_mirror_push`: the name of a server-side env var the tool resolves at call time (`os.Getenv`), so the mirror credential **never appears in the tool-call payload** (→ transcript → claudewatcher → brain → gitea history). An env name that resolves to empty **errors loudly** (`ErrValidation`) instead of silently sending an empty password. Raw `remote_password` still works but the schema now marks it **DISCOURAGED** and the descriptor steers callers to the env path. Tests: `TestRepoMirrorPushTool_PasswordFromEnv` (secret resolved from the env var, never in output) + `TestRepoMirrorPushTool_EnvUnsetErrors`. `task check` green (exit 0, 0 FAIL — verified). **Infra follow-up (out of scope for this code change):** for `remote_password_env` to resolve on the live server, the gitea-mcp deployment must actually carry the referenced env var (e.g. a GitHub mirror PAT injected via ESO into the pod env), then callers pass just the var *name*. Until that secret is wired into the deployment, the raw path or the REST-in-`op run` workaround remains. Filing/So noting — the code accepts the reference now; provisioning the pod env is the remaining infra step. Closing #49.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#49