AgentSkill: run a repo-specific security audit of the OpenHands-Tab VS Code extension with a focus on secret handling, webview/host boundaries, persistence, and logging redaction. Trigger with /audit.
This is a repeatable checklist for an agent to run against this repository.
When invoked (e.g. the user types /audit), follow the checklist below, inspect the referenced code, and then output a short audit report.
Repo-specific note: This repo supports AgentSkills-style
SKILL.mddirectories, but also uses additional OpenHands-specific frontmatter fields liketriggers. Do not assume every AgentSkills client will understand those extra fields.
Return a report with:
Never include real secret values in the report (including partial tokens).
Also: do not paste raw grep output that might contain secrets into the report. Prefer file paths + line numbers only.
Primary risks for this VS Code extension:
Do not trust hardcoded lists in docs. Investigate and find the set of provider key names in the codebase.
src/webview/host/handlers/secretHelpers.ts
getProviderApiKeyName(...)).src/settings/SettingsManager.ts, src/settings/VscodeSettingsAdapter.ts
adapter.storeSecret(...) / context.secrets.store(...).Check: no secret values are written via workspace.getConfiguration().update(...) (or any other settings persistence).
src/settings/VscodeSettingsAdapter.tssrc/settings/SettingsManager.tssrc/extension/secretCommands.tsPass criteria:
context.secrets.store(...) / context.secrets.delete(...).Fail examples:
Check: session keys are not “global”; they must be bound to a specific normalized server URL.
src/auth/serverSessionApiKeys.tssrc/shared/serverUrls.ts (normalization rules)src/extension/secretCommands.ts (set/migration behavior)Pass criteria:
normalizeServerUrl(...) supports both http: and https:localhost, 127.0.0.1, ::1) and https for non-localFail examples:
openhands.sessionApiKey applied to whichever server URL is currently configured.Non-negotiable policy: the extension host must never send secret values to the webview.
Reality check: the webview may still see a secret if the user types it into a webview input field. Treat that as the maximum tolerated exposure, not a convenience.
src/webview-src/components/app/useLlmProfilesRequests.ts (llmProfileApiKeySetRequest includes apiKey)src/webview/host/handlers/llmProfiles.ts (handleLlmProfileApiKeySetRequest stores to context.secrets)src/webview-src/components/LlmProfilesView.tsxPass criteria:
acquireVsCodeApi().setState(...) storing apiKey, headers, or tokensconsole.* printing request payloads that may contain secretsFail examples:
In this repo, LLM profiles are persisted to disk under ~/.openhands/llm-profiles/*.json.
Secrets can appear in profiles in two ways:
apiKeyRef.kind="inline" (literal secret value) — high riskheaders (Authorization, x-api-key, etc.) — high riskNon-secret references are expected to use:
apiKeyRef.kind="key" (reference name resolved via SecretRegistry / SecretStorage / env)
Files to inspect:
src/webview/host/llmProfilesStore.tspackages/agent-sdk/src/sdk/llm/profiles.tspackages/agent-sdk/src/sdk/llm/types.ts (ApiKeyRef)Check: profile save paths have a default mode that excludes secrets.
Pass criteria:
includeSecrets exists and defaults to false.includeSecrets=false:
headers are removed by default.apiKeyRef.kind="inline" is removed by default.apiKeyRef.kind="key" is preserved (it is a reference name, not the secret value).Fail examples:
llmProfileLoadResponse.Rule: the primary defense is do not log secrets at all. Redaction is a backstop.
Also: do not treat allowlists as a security boundary unless enforced in code. In this repo, allowed-tools is parsed for AgentSkills parity but is not used to restrict tool execution.
src/extension/devBridgeLogger.ts (createMaskedOutputChannel)src/extension/debugJsonOutputChannel.tssrc/shared/safeStringify.tssrc/shared/maskSecrets.tsChecks:
console.* printing request payloads that may contain secrets.Audit technique:
console.(log|warn|error) in both src/ and src/webview-src/outputChannel.append/appendLine/replaceJSON.stringify(...) used in logsmaskSecretsInText(...) and/or safeStringify(...).Fail examples:
llmProfileApiKeySetRequest.Checks:
token=, api_key=, key=, authorization=).Repo-specific note:
http: server URLs (see src/shared/serverUrls.ts). Verify:
http:// due to scheme defaultingFail examples:
?token=... in any URL.http://... due to missing scheme.Conversation stores are a persistence risk because they can contain:
user messages (which may contain secrets)
tool results / logs
LLM request/response payloads (which may include headers)
Files to inspect:
src/extension/conversationStoreRoot.tssrc/webview/host/conversationHistory.tsPass criteria:
0700) and files are restricted (e.g. 0600) when the platform supports it.Because secrets may exist transiently in webview JS memory, treat webview XSS and unsafe openers as credential leak vectors.
src/webview/getWebviewHtml.tssrc/webview/host/handlers/openers.ts (handleOpenMarkdownLink)src/__tests__/openMarkdownLink.security.test.tsPass criteria:
default-src 'none' pattern).javascript:, file://) and block path traversal outside the workspace.These are assistive checks (expect false positives/negatives). The real audit is code-path based.
Safety rule: avoid printing raw matching lines (they may contain secrets). Prefer
file:lineonly.
Find potential secret patterns in code (output paths + line numbers only):
grep -RInE "sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9]{12,}|github_pat_[A-Za-z0-9_]{12,}|AIza[A-Za-z0-9_-]{12,}|(AKIA|ASIA)[A-Z0-9]{16}|eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]*" src packages | cut -d: -f1-2 | sort -uFind code paths that can write to disk:
grep -RInE "\\.writeFile(Sync)?\\(|\\.appendFile(Sync)?\\(" src packages | cut -d: -f1-2 | sort -uFind obvious logging sites:
grep -RInE "console\\.(log|warn|error)" src packages src/webview-src | cut -d: -f1-2 | sort -uRun tests:
npm testThese patterns are considered good practice in this repo; if present, explicitly list them under “Good practices already present”:
context.secrets.*) and never written to settings JSON.maskSecretsInText, safeStringify) used for every logging surface.apiKeyRef.kind="inline") or headers.globalState, workspaceState, workspace.getConfiguration(), or any JSON fileallowed-tools) is enforced when it is notThis section documents current reality (today) in this repo: the name apiKey appears in multiple domains and does not always mean the same thing.
This is a source of audit mistakes and security regressions. We should improve this over time (clearer naming and explicit types/formats), but in the meantime we must be careful to not make it worse.
| Domain / meaning | Where it lives (structure) | Typical source (where it comes from) | Sinks (where it ends up) | What can go wrong |
|---|---|---|---|---|
| LLM provider credential (OpenAI/Anthropic/Gemini/etc.) | LLMConfiguration.apiKeyRef (packages/agent-sdk/src/sdk/llm/types.ts) and on-disk profile JSON (~/.openhands/llm-profiles/*.json when includeSecrets=true) | From SecretStorage / SecretRegistry, or explicit inline opt-in (apiKeyRef.kind="inline") | Network requests to provider clients (Authorization, x-api-key, x-goog-api-key) | Easy to accidentally persist to disk or log. Treat apiKeyRef.kind="inline" as a secret; treat apiKeyRef.kind="key" as a reference name.
| Webview → host payload secret (user typed key) | Webview message: llmProfileApiKeySetRequest.apiKey (src/shared/webviewMessages.ts) | User types into webview UI (src/webview-src/components/LlmProfilesView.tsx) | Stored in context.secrets.store(...) and optionally secretRegistry.set(...) (src/webview/host/handlers/llmProfiles.ts) | Webview JS memory is a leak surface (console logs, devtools, XSS, persistence). Host must never echo secrets back to webview.
| Agent-server session API key (auth to OpenHands server) | RemoteWorkspaceOptions.sessionApiKey (packages/agent-sdk/src/workspace/RemoteWorkspace.ts) and SecretStorage keys derived from normalized server URL (src/auth/serverSessionApiKeys.ts) | settings.secrets.sessionApiKey (remote conversation setup) | Network requests to agent-server via X-Session-API-Key / Authorization: Bearer ... | Confusing it with provider credentials can cause wrong-host leakage. Ensure per-server scoping and never attach to unintended hosts/redirects.
| HAL / auxiliary service keys (Gemini classifier, ElevenLabs, etc.) | Feature params objects (e.g. src/hal/gemini/decisionClassifier.ts, src/hal/elevenlabs/ttsClient.ts) | From SecretStorage-backed settings / secrets | Outbound requests to those services | Same logging/persistence risks, plus accidental reuse in unrelated contexts.
apiKey fields/messages without strong justificationsessionApiKey, providerApiKey, ttsApiKey, etc.) and explicit reference types (apiKeyRef) over heuristicsIf you suspect a leak but cannot prove it, write it as a risk hypothesis and point to the exact file + code region to inspect next.
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer