Skip to content

Managing Secrets

Z-M-Huang edited this page Mar 19, 2026 · 3 revisions

Managing Secrets

Secrets allow action scripts to authenticate with APIs without exposing credentials. Every secret is scoped to specific URL patterns, so it can only be used with the intended services.

Declaring Secrets

Declare secrets in your manifest's secrets section:

secrets:
  - name: GITHUB_TOKEN
    description: "GitHub personal access token for API access"
    scope: "https://api.github.com/*"
    required: false

  - name: API_KEY
    description: "Backend API key"
    scope:
      - "https://api.myservice.com/*"
      - "https://staging.myservice.com/*"
    required: true

Secret Fields

Field Type Required Description
name string Yes Secret identifier (used in ctx.getSecret)
description string No Explains what this secret is and where to get it
scope string or string[] Yes URL pattern(s) where the secret can be used
required boolean No Default: false. If true, server won't start without it

Scope Patterns

Scope patterns control which URLs a secret can be sent to. This prevents accidental credential leakage.

Pattern Syntax

A scope pattern is a URL prefix with an optional trailing wildcard (*).

# Exact URL match
scope: "https://api.example.com/v1/auth"

# All paths under a domain
scope: "https://api.example.com/*"

# Specific path prefix
scope: "https://api.example.com/v2/admin/*"

How Matching Works

  • The wildcard * matches at path boundaries/* won't match /repos-private
  • Exact origin requiredapi.example.com won't match evil.api.example.com
  • No subdomain matching — scope for example.com won't match sub.example.com
# Given scope: "https://api.github.com/*"
# These MATCH:
#   https://api.github.com/repos/owner/repo
#   https://api.github.com/search/code?q=test
#   https://api.github.com/

# These DO NOT MATCH:
#   https://evil-api.github.com/repos          (different subdomain)
#   http://api.github.com/repos                (wrong scheme)
#   https://api.github.com.evil.com/repos      (different domain)

Multiple Scopes

Use an array when a secret needs to work with multiple services:

secrets:
  - name: GITHUB_TOKEN
    description: "GitHub token for API and raw content access"
    scope:
      - "https://api.github.com/*"
      - "https://raw.githubusercontent.com/*"
    required: false

Scope Best Practices

# GOOD: Specific scope for the service you're using
scope: "https://api.github.com/*"

# GOOD: Multiple specific scopes
scope:
  - "https://api.stripe.com/*"
  - "https://dashboard.stripe.com/api/*"

# BAD: Too broad — would allow the secret to be sent anywhere on the domain
scope: "https://*"

# BAD: Includes unintended paths
scope: "https://api.github.com/repos"
# This won't match /repos/owner/name — you probably want /repos/*

Providing Secrets at Runtime

Command-Line Flag

git-doc-mcp --manifest ./manifest.yml --secret GITHUB_TOKEN=ghp_abc123

Multiple secrets:

git-doc-mcp --manifest ./manifest.yml \
  --secret GITHUB_TOKEN=ghp_abc123 \
  --secret API_KEY=sk-xyz789

Environment Variables

Secrets are automatically read from environment variables with the GIT_MCP_SECRET_ prefix:

export GIT_MCP_SECRET_GITHUB_TOKEN=ghp_abc123
export GIT_MCP_SECRET_API_KEY=sk-xyz789
git-doc-mcp --manifest ./manifest.yml

Missing Required Secrets

If a required secret is not provided via flags or environment variables, the server refuses to start and exits with an error:

Error: Required secret "GITHUB_TOKEN" not provided

Precedence

  1. --secret flags (highest priority)
  2. GIT_MCP_SECRET_* environment variables
  3. If still missing and required: true — server exits with error

Using Secrets in Actions

Access secrets via ctx.getSecret(name, url):

export default async function callApi(input, ctx) {
  const url = `https://api.github.com/repos/${input.owner}/${input.repo}`;

  // getSecret returns the value if:
  // 1. The secret exists and was provided by the user
  // 2. The URL matches the secret's scope pattern
  // Otherwise returns undefined
  const token = ctx.getSecret('GITHUB_TOKEN', url);

  const headers = { 'Accept': 'application/vnd.github.v3+json' };
  if (token) {
    headers['Authorization'] = `token ${token}`;
  }

  const response = await ctx.fetch(url, { headers });
  // ...
}

Key Behaviors

  • getSecret always takes TWO arguments: the secret name and the URL you'll use it with
  • Returns undefined if the secret wasn't provided, even if it's in the manifest
  • Returns undefined if the URL doesn't match the secret's scope, even if the secret was provided
  • All secret access attempts are audit-logged (including denials)
  • Secret scopes are URL patterns for ctx.getSecret(name, url) calls -- they are not affected by whether the action itself was loaded from a local file path or a remote URL. The scope validation always applies to the url argument passed to getSecret.

Pattern: Optional Authentication

Most tools should work without secrets (reduced functionality) and enhance with secrets:

export default async function search(input, ctx) {
  const url = `https://api.github.com/search/code?q=${encodeURIComponent(input.query)}`;

  const headers = { 'Accept': 'application/vnd.github.v3+json' };
  const token = ctx.getSecret('GITHUB_TOKEN', url);

  if (token) {
    headers['Authorization'] = `token ${token}`;
    ctx.log('debug', 'Authenticated (5000 req/hr limit)');
  } else {
    ctx.log('info', 'Unauthenticated (60 req/hr limit)');
  }

  const response = await ctx.fetch(url, { headers });

  if (!response.ok && response.status === 403) {
    return {
      content: [{
        type: 'text',
        text: token
          ? 'Rate limit exceeded even with authentication.'
          : 'Rate limit exceeded. Provide a GITHUB_TOKEN for higher limits.',
      }],
      isError: true,
    };
  }

  // ...
}

Security Model

Secret Isolation

  • Secrets are never passed to action scripts directly
  • ctx.getSecret validates the URL scope before returning the value
  • The sandbox cannot access secrets except through ctx.getSecret
  • Cross-origin redirects strip Authorization and Cookie headers

Audit Trail

Every getSecret call is logged:

  • Secret name
  • URL it was requested for
  • Whether access was granted or denied
  • Manifest name

Logs are written to ~/.git-doc-mcp/logs/audit.jsonl.

Clone this wiki locally