From 56c8ae7d882420ba99abb7233e147e210dc87415 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Tue, 28 Oct 2025 11:52:18 -0700 Subject: [PATCH] Add show_full_output option to control output verbosity (#580) * Add show_full_output option to control output verbosity * Update base-action/src/run-claude.ts Co-authored-by: Ashwin Bhat * Wire show_full_output through to base-action * Document show_full_output security warnings in docs/security.md --------- Co-authored-by: Ashwin Bhat --- action.yml | 5 ++ base-action/README.md | 43 ++++++++-------- base-action/action.yml | 5 ++ base-action/src/index.ts | 1 + base-action/src/run-claude.ts | 94 +++++++++++++++++++++++++++++++---- docs/security.md | 28 +++++++++++ 6 files changed, 147 insertions(+), 29 deletions(-) diff --git a/action.yml b/action.yml index fef543e..5deae7c 100644 --- a/action.yml +++ b/action.yml @@ -101,6 +101,10 @@ inputs: description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment." required: false default: "" + show_full_output: + description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments." + required: false + default: "false" plugins: description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')" required: false @@ -221,6 +225,7 @@ runs: INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }} INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} + INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} INPUT_PLUGINS: ${{ inputs.plugins }} INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }} diff --git a/base-action/README.md b/base-action/README.md index 40e4035..0889fa1 100644 --- a/base-action/README.md +++ b/base-action/README.md @@ -85,29 +85,32 @@ Add the following to your workflow file: ## Inputs -| Input | Description | Required | Default | -| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ---------------------------- | -| `prompt` | The prompt to send to Claude Code | No\* | '' | -| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' | -| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' | -| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' | -| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | -| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' | -| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' | -| `system_prompt` | Override system prompt | No | '' | -| `append_system_prompt` | Append to system prompt | No | '' | -| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' | -| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' | -| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' | -| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' | -| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' | -| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' | -| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' | +| Input | Description | Required | Default | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- | +| `prompt` | The prompt to send to Claude Code | No\* | '' | +| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' | +| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' | +| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' | +| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | +| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' | +| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' | +| `system_prompt` | Override system prompt | No | '' | +| `append_system_prompt` | Append to system prompt | No | '' | +| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' | +| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' | +| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' | +| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' | +| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' | +| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' | +| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' | +| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* | \*Either `prompt` or `prompt_file` must be provided, but not both. +\*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#️-full-output-security-warning) for important security considerations. + ## Outputs | Output | Description | diff --git a/base-action/action.yml b/base-action/action.yml index a549603..e8cd9a3 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -55,6 +55,10 @@ inputs: description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment." required: false default: "" + show_full_output: + description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments." + required: false + default: "false" plugins: description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')" required: false @@ -134,6 +138,7 @@ runs: INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }} INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} + INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} INPUT_PLUGINS: ${{ inputs.plugins }} INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }} diff --git a/base-action/src/index.ts b/base-action/src/index.ts index 87a32d6..fdd1406 100644 --- a/base-action/src/index.ts +++ b/base-action/src/index.ts @@ -41,6 +41,7 @@ async function run() { model: process.env.ANTHROPIC_MODEL, pathToClaudeCodeExecutable: process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE, + showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, }); } catch (error) { core.setFailed(`Action failed with error: ${error}`); diff --git a/base-action/src/run-claude.ts b/base-action/src/run-claude.ts index 58c58c0..2ffbc19 100644 --- a/base-action/src/run-claude.ts +++ b/base-action/src/run-claude.ts @@ -12,6 +12,59 @@ const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`; const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`; const BASE_ARGS = ["--verbose", "--output-format", "stream-json"]; +/** + * Sanitizes JSON output to remove sensitive information when full output is disabled + * Returns a safe summary message or null if the message should be completely suppressed + */ +function sanitizeJsonOutput( + jsonObj: any, + showFullOutput: boolean, +): string | null { + if (showFullOutput) { + // In full output mode, return the full JSON + return JSON.stringify(jsonObj, null, 2); + } + + // In non-full-output mode, provide minimal safe output + const type = jsonObj.type; + const subtype = jsonObj.subtype; + + // System initialization - safe to show + if (type === "system" && subtype === "init") { + return JSON.stringify( + { + type: "system", + subtype: "init", + message: "Claude Code initialized", + model: jsonObj.model || "unknown", + }, + null, + 2, + ); + } + + // Result messages - Always show the final result + if (type === "result") { + // These messages contain the final result and should always be visible + return JSON.stringify( + { + type: "result", + subtype: jsonObj.subtype, + is_error: jsonObj.is_error, + duration_ms: jsonObj.duration_ms, + num_turns: jsonObj.num_turns, + total_cost_usd: jsonObj.total_cost_usd, + permission_denials: jsonObj.permission_denials, + }, + null, + 2, + ); + } + + // For any other message types, suppress completely in non-full-output mode + return null; +} + export type ClaudeOptions = { claudeArgs?: string; model?: string; @@ -24,6 +77,7 @@ export type ClaudeOptions = { appendSystemPrompt?: string; claudeEnv?: string; fallbackModel?: string; + showFullOutput?: string; }; type PreparedConfig = { @@ -138,12 +192,27 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) { pipeStream.destroy(); }); + // Determine if full output should be shown + // Show full output if explicitly set to "true" OR if GitHub Actions debug mode is enabled + const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true"; + let showFullOutput = options.showFullOutput === "true" || isDebugMode; + + if (isDebugMode && options.showFullOutput !== "false") { + console.log("Debug mode detected - showing full output"); + showFullOutput = true; + } else if (!showFullOutput) { + console.log("Running Claude Code (full output hidden for security)..."); + console.log( + "Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.", + ); + } + // Capture output for parsing execution metrics let output = ""; claudeProcess.stdout.on("data", (data) => { const text = data.toString(); - // Try to parse as JSON and pretty print if it's on a single line + // Try to parse as JSON and handle based on verbose setting const lines = text.split("\n"); lines.forEach((line: string, index: number) => { if (line.trim() === "") return; @@ -151,17 +220,24 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) { try { // Check if this line is a JSON object const parsed = JSON.parse(line); - const prettyJson = JSON.stringify(parsed, null, 2); - process.stdout.write(prettyJson); - if (index < lines.length - 1 || text.endsWith("\n")) { - process.stdout.write("\n"); + const sanitizedOutput = sanitizeJsonOutput(parsed, showFullOutput); + + if (sanitizedOutput) { + process.stdout.write(sanitizedOutput); + if (index < lines.length - 1 || text.endsWith("\n")) { + process.stdout.write("\n"); + } } } catch (e) { - // Not a JSON object, print as is - process.stdout.write(line); - if (index < lines.length - 1 || text.endsWith("\n")) { - process.stdout.write("\n"); + // Not a JSON object + if (showFullOutput) { + // In full output mode, print as is + process.stdout.write(line); + if (index < lines.length - 1 || text.endsWith("\n")) { + process.stdout.write("\n"); + } } + // In non-full-output mode, suppress non-JSON output } }); diff --git a/docs/security.md b/docs/security.md index fe71889..dcc95b7 100644 --- a/docs/security.md +++ b/docs/security.md @@ -56,3 +56,31 @@ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable! claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable! ``` + +## ⚠️ Full Output Security Warning + +The `show_full_output` option is **disabled by default** for security reasons. When enabled, it outputs ALL Claude Code messages including: + +- Full outputs from tool executions (e.g., `ps`, `env`, file reads) +- API responses that may contain tokens or credentials +- File contents that may include secrets +- Command outputs that may expose sensitive system information + +**These logs are publicly visible in GitHub Actions for public repositories!** + +### Automatic Enabling in Debug Mode + +Full output is **automatically enabled** when GitHub Actions debug mode is active (when `ACTIONS_STEP_DEBUG` secret is set to `true`). This helps with debugging but carries the same security risks. + +### When to Enable Full Output + +Only enable `show_full_output: true` or GitHub Actions debug mode when: + +- Working in a private repository with controlled access +- Debugging issues in a non-production environment +- You have verified no secrets will be exposed in the output +- You understand the security implications + +### Recommended Practice + +For debugging, prefer using `show_full_output: false` (the default) and rely on Claude Code's sanitized output, which shows only essential information like errors and completion status without exposing sensitive data.