Skip to content

Security: exec tool returns raw stdout/stderr to agent without secret redaction #71211

@nhaener

Description

@nhaener

Bug Report: exec tool returns raw stdout/stderr to agent without secret redaction

Summary

Foreground exec results in OpenClaw are assembled directly from aggregated command output and returned to the agent without an obvious secret-redaction pass. If a command prints secrets, those secrets can enter internal tool/agent context.

Severity

High, internal secret exposure risk

Impact

  • Any exec command that prints .env contents, tokens, API keys, passwords, or other sensitive output can expose that data to the agent runtime.
  • Chat surfaces may or may not separately suppress rendering, but the exec tool path itself appears unsafe.
  • This appears to affect more than one agent/runtime, not a single isolated profile.

Confirmed behavior

In the exec implementation, foreground output is returned via logic equivalent to:

function buildExecForegroundResult(params) {
	const warningText = params.warningText?.trim() ? `${warningText}\n\n` : "";
	if (params.outcome.status === "failed") return failedTextResult(`${warningText}${params.outcome.reason}`, {
		status: "failed",
		exitCode: params.outcome.exitCode ?? null,
		durationMs: params.outcome.durationMs,
		aggregated: params.outcome.aggregated,
		timedOut: params.outcome.timedOut,
		cwd: params.cwd
	});
	return textResult(`${warningText}${params.outcome.aggregated || "(no output)"}`, {
		status: "completed",
		exitCode: params.outcome.exitCode,
		durationMs: params.outcome.durationMs,
		aggregated: params.outcome.aggregated,
		cwd: params.cwd
	});
}

And this is used directly after process completion:

run.promise.then((outcome) => {
	...
	resolve(buildExecForegroundResult({
		outcome,
		cwd: run.session.cwd,
		warningText: getWarningText()
	}));
})

This means params.outcome.aggregated is passed straight into the tool result text.

Related files traced

  • Tool wrapper:
    • dist/pi-tools-*.js
  • Exec implementation:
    • dist/bash-tools-*.js
  • Upstream exec runtime:
    • dist/bash-tools.exec-runtime-*.js

What is not the issue

  • ${SECRET} indirection in config is working as expected.
  • EnvironmentFile=... rendering is not itself the leak.
  • There is env sanitization for process env overrides, but that only controls what env is passed into the command, not what the command prints back out.

Evidence from code review

I found sanitization/redaction paths for:

  • config snapshots
  • logs
  • chat history
  • exec approval display text

I did not find a dedicated secret-redaction pass for generic exec stdout/stderr before tool result delivery.

Reproduction idea

Run a command that prints a secret-bearing env file, for example:

  • cat /path/to/.env
  • grep TOKEN /path/to/.env

Observed behavior:

  • output is incorporated into the exec tool result returned to the agent

Expected behavior:

  • either block obviously sensitive file reads, or
  • redact secret-looking values before tool result delivery, or
  • require explicit opt-in to reveal raw secret-bearing stdout

Recommended fix

Best fix points:

  1. Primary: redact secrets before buildExecForegroundResult(...) emits params.outcome.aggregated
  2. Also recommended: redact at the lower runtime layer when constructing outcome.aggregated

Suggested protections

  • redact env-style assignments like:
    • KEY=value
    • especially keys matching TOKEN, SECRET, PASSWORD, API_KEY, AUTH, PAT
  • redact known secret formats:
    • bot tokens
    • provider API keys
    • bearer tokens
    • hex secrets
  • optionally hard-block raw reads of common secret files:
    • .env
    • generated env files
    • credential stores

Real-world consequence seen

A diagnostic env-file read produced raw secret-bearing stdout into internal tool context during live troubleshooting. User-facing chat may have stayed clean, but the internal exposure path is confirmed and should be treated as a security bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions