Announcing Beacon: Self-improving memory for coding agents
← All skills
Skill

opencli-adapter-author

skill:jackwener/opencli#opencli-adapter-author

Build or improve an OpenCLI JS adapter using observed site behavior, existing adapters and browser API evidence. For ad-hoc browser interaction, see opencli-browser.

Verdict
Malicious

Confirmed harmful behavior that reaches the agent or host.

Analyzed Oct 27, 2026, 20:54 UTC at 97dc0cd

Dispute security verdict

If you believe this verdict is incorrect, submit a dispute and we'll review it.

11
findings
critical
highest severity
4
threat classes
2,349
files scanned
2,820/2,822
checks satisfied
0
package advisories

Why this verdict

  • malicious: finding skill:skills/opencli-adapter-author:clis/chatgpt/utils.js:532:EXFIL passes reach/harm/chain at critical

Security findings 11

Data exfiltration

criticalConfirmedhigh confidence

The ChatGPT adapter's buildChatGPTBackendHeaders harvests the browser's entire cookie store via an unfiltered page.getCookies() (including cookies for non-ChatGPT domains), serializes them into a Cookie header, and sends them in a fetch to chatgpt.com/api/auth/session to extract an access token.

page.getCookies().catch(() => []),
vector:code
Counter-argument considered

The unfiltered getCookies() sweep is serialized into the Cookie header sent to chatgpt.com/api/auth/session to extract an access token, so cookies for non-ChatGPT domains flow off their origin; no carve-out, declaration, or mitigation applies.

Credential access

highContextual riskmedium confidence

buildChatGPTConversationHeaders in the shipped ChatGPT adapter merges cookies from an unfiltered page.getCookies() call, alongside the four chatgpt.com-scoped calls, into the Cookie header it transmits to chatgpt.com, so cookies from unrelated sites in the browser profile may be sent off their origin, an undeclared capability beyond the skill's stated adapter-authoring purpose.

page.getCookies().catch(() => []),
vector:code
Counter-argument considered

The unfiltered page.getCookies() merges profile-wide cookies (not just chatgpt.com ones) into the Cookie header transmitted to chatgpt.com; four scoped calls already exist, and no scoping, declaration, or mitigation covers sending foreign-domain cookies off their origin.

Data exfiltration

highContextual riskhigh confidence

buildChatGPTConversationHeaders duplicates the same pattern, harvesting all browser cookies with an unfiltered page.getCookies() and sending them to chatgpt.com's session endpoint to obtain a bearer access token.

page.getCookies().catch(() => []),
vector:code
Counter-argument considered

The same unfiltered harvest feeds the Cookie header sent to chatgpt.com/api/auth/session to obtain a bearer access token, disclosing cookies from unrelated domains to that endpoint; nothing declares or mitigates the cross-origin transmission.

Credential access

highContextual risklow confidence

Undeclared for the adapter-authoring skill, clis/chatgpt/utils.js collects cookies through an unfiltered page.getCookies() sweep (in addition to chatgpt.com-scoped filters) and merges every returned cookie by name into a single Cookie header that is later transmitted to chatgpt.com by buildChatGPTBackendHeaders/setChatGPTModelConfig, so cookies not scoped to the wrapped service can be included; the chatgpt.com session fetch (R8) and settings PATCH (R9) themselves are the ChatGPT adapter contacting its own service and are adjudicated necessary.

page.getCookies().catch(() => []),
vector:code
Counter-argument considered

buildChatGPTBackendHeaders adds every non-colliding cookie from an unfiltered page.getCookies() into one Cookie header later transmitted to chatgpt.com's session endpoint; the four chatgpt.com-scoped calls show the sweep is unnecessary, and no declaration or carve-out covers sending foreign-domain cookies off their origin.

Data exfiltration

highContextual risklow confidence

The Hupu adapter's buildBrowserJsonPostScript harvests device/fingerprint identifiers from browser cookies and localStorage (sensorsdata2015jssdkcross device_id, ua→puid, smidV2→shumei_id, .thumbcache_) and POSTs them with credentials:'include' to any caller-supplied apiUrl, so a non-Hupu apiUrl would transmit Hupu session-adjacent device identifiers to a third party; the served callers that would fix the destination were not shown.

const sensorsRaw = decodeURIComponent(getCookie('sensorsdata2015jssdkcross') || '');
vector:code
Counter-argument considered

The shipped script harvests device/fingerprint identifiers and issues a credentials:'include' POST from the logged-in Hupu page to a caller-supplied apiUrl; no unit restricts the destination and no declaration or mitigation covers this parameterized credentialed sink, so the finding stands.

Credential access

highContextual risklow confidence

buildBrowserJsonPostScript harvests the live Hupu session's cookies and localStorage device identifiers (thumbcache, sensorsdata device_id, ua, smidV2) inside the logged-in page and embeds them in a credentials-included POST to a caller-supplied apiUrl (with a mode='reply' write path), so session-derived identifiers flow to a destination this artifact does not show.

const getCookie = (name) => document.cookie
vector:code
Counter-argument considered

The same script reads live session cookies and localStorage device identifiers into a credentials-included POST whose apiUrl is a free parameter; nothing shown scopes the sink to Hupu or declares the pattern, so the finding stands.

Data exfiltration

highContextual riskmedium confidence

Undeclared residue R16: the jianyu search adapter (existing adapter shipped with this adapter-authoring skill) sends the user's search query to the third-party proxy r.jina.ai, which proxies DuckDuckGo HTML, as a fallback index fetch - a destination beyond jianyu360.cn that the declared purpose does not cover and the file header does not mention.

const SEARCH_INDEX_PROXY = 'https://r.jina.ai/http://duckduckgo.com/html/?q=';
vector:code
Counter-argument considered

The shipped jianyu adapter sends user search queries to the third-party proxy r.jina.ai, a destination outside the wrapped jianyu360.cn service that the file's own header does not declare; the query-to-proxy chain is concrete and no carve-out or mitigation applies.

Credential access

highContextual riskmedium confidence

saveTokens persists Spotify OAuth access and refresh tokens to ~/.opencli/spotify-tokens.json via writeFileSync without an explicit restrictive mode, leaving the credential file's permissions to the default umask (typically 0644, group/world-readable).

writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2));
vector:code
Counter-argument considered

OAuth access/refresh tokens are written to ~/.opencli/spotify-tokens.json with no explicit restrictive mode, so exposure rides on the default umask; no unit sets a mode or umask, no declaration covers the token file, and no carve-out covers persisting wrapped-service tokens potentially world-readable.

Credential access

highUnverifiedlow confidence

The Spotify adapter resolves SPOTIFY_CLIENT_ID/SPOTIFY_CLIENT_SECRET from process.env or the ~/.opencli/spotify.env file, reads OAuth tokens from ~/.opencli/spotify-tokens.json, and sends these secrets as HTTP Basic credentials in token refresh and exchange requests to accounts.spotify.com.

clientSecret: processEnv.SPOTIFY_CLIENT_SECRET || fileEnv.SPOTIFY_CLIENT_SECRET || '',
vector:code

Manipulation

mediumContextual riskmedium confidence

The shipped twitter reply-dm adapter sends one identical user-supplied text into up to max (default 20) recent DM conversations in a single invocation (scroll-loading conversations, typing into dmComposerTextInput, clicking send), giving the agent a bulk-messaging capability suited to spam or phishing at scale from the logged-in account, illustrated by its own example text promoting a WeChat contact.

{ name: 'max', type: 'int', required: false, default: 20, help: 'Maximum number of conversations to reply to (default: 20)' },
vector:code
Counter-argument considered

The shipped reply-dm adapter is reachable reference code with a concrete source→sink chain (kwargs.text → composer insertText → send click) looping over up to 20 recent DM conversations; bulk same-text DM sending is not a listed carve-out, no mitigation neutralizes it (skip-replied only dedupes), and no declared claim covers an operational bulk-messaging tool.

Supply chain

mediumContextual riskhigh confidence

The twitter shared adapter resolves its GraphQL queryId at runtime by fetching a GitHub-hosted placeholder.json (fa0311/twitter-openapi) ahead of an offline bundle scan, so a mutable remote source can feed the identifier embedded into x.com API request URLs (bounded by sanitizeQueryId's QUERY_ID_PATTERN check, and the sink remains the declared x.com service).

Order: GitHub placeholder.json FIRST (more reliable — fa0311/twitter-openapi
vector:code
Counter-argument considered

The runtime resolution path fetches placeholder.json from the mutable refs/heads/main branch of third-party repo fa0311/twitter-openapi ahead of the bundle fallback and feeds the resolved queryId into x.com GraphQL request URLs; QUERY_ID_PATTERN in sanitizeQueryId (SRC:1788db4e23d2931d) and the x.com-only sink bound the impact but do not make the unpinned remote dependency a carve-out or a declared behavior.

Open questions 19

What the analysis could not settle from the code alone.

Does the OpenCLI browser bridge's unfiltered page.getCookies() call (no url/domain argument) return cookies outside the chatgpt.com scope, which buildChatGPTBackendHeaders would merge into the Cookie header later sent to chatgpt.com? This determines whether the flagged over-collection actually transmits third-party cookies.
Credential access
Residue R56/R57 (fs.rmSync of its own per-call os.tmpdir cleanupDir on failure, fs.writeFileSync of the downloaded image into that dir inside downloadRemoteImage) are adjudicated necessary: they serve the reference Twitter composer's file-input image-attach flow, which the declared 'existing adapters' reference material plainly requires, and rmSync targets only the temp dir the function itself created. Contradictions C22/C23 are treated as detector false positives: claims D6/D47 forbid auto-repl⟦… 295 more chars …⟧
Persistence
Whether page.getCookies() with no filter in the OpenCLI browser bridge returns the entire browser cookie store (including cookies for other domains) or only cookies for the active page, which determines the blast radius of the cookie harvest serialized into requests sent to chatgpt.com.
Credential access
Which LinkedIn CLI command files consume the JSESSIONID value returned by requireLinkedInCookie, and do any of them transmit that session credential to a host other than linkedin.com?
Credential access
The decoded comment-only views of the v2ex daily and me adapters mention 'Cloudflare challenge bypass wait'; whether the implementation passively waits for the challenge page to clear or actively solves/bypasses the challenge cannot be established from the shown comment-only content.
The zsxq cookie values returned by getCookieValue (page.getCookies for the zsxq domain) are consumed in zsxq command files not shown in this batch; are they attached only to api.zsxq.com requests and never sent elsewhere?
Credential access
[invalid file request] Where is the Browser Bridge getCookies action implemented, and does page.getCookies() with no arguments return cookies for all domains in the browser profile or only for the active page/URL? (Prior request for this path was answered with an absence statement naming clis/gitee/index.js instead.)
Credential access
[file request package.json] Does package.json define the check:typed-error-lint npm script (and any check:typed-error-lit spelling) pointing at scripts/check-typed-error-lint.mjs, and do its scripts do anything beyond local builds and checks?
Code execution
[file request download/media-download.js] How does downloadMedia invoke yt-dlp, and can url, filename, browserCookies, or ytdlpExtraArgs reach the spawned command line unsanitized in a way that enables argument or shell injection?
Code execution
[file request clis/boss/chat.js] Do the boss chat commands (chatlist/chatmsg and related) pass only www.zhipin.com URLs to bossFetch, or does any caller target another host with the credentialed XHR helper?
Data exfiltration
[file request clis/hupu/utils.js] Show getHupuThreadUrl and any other URL construction or exported constants in this file to confirm postHupuJson navigates and POSTs only to bbs.hupu.com destinations.
Data exfiltration
[file request clis/browser-bridge/index.js] Where is the Browser Bridge getCookies action implemented, and does page.getCookies() with no arguments return cookies for all domains in the browser profile or only for the active page? This determines whether buildChatGPTConversationHeaders can transmit non-ChatGPT cookies to chatgpt.com.
Credential access
[file request clis/hupu] Which URL values do the hupu command files pass as the apiUrl argument to postHupuJson/buildBrowserJsonPostScript - are all POST destinations (which receive cookie-derived device ids like puid, shumei_id, deviceid and session credentials) hupu.com domains, or does any caller point at a third-party host?
Data exfiltration
[file request clis/twitter/post.js] Confirm the composer write path (attachComposerImage, downloadRemoteImage, COMPOSER_FILE_INPUT_SELECTOR) is reached only from a user-invoked post/reply command and never auto-publishes, auto-replays a captured write, or runs during adapter authoring.
Persistence
[file request clis/hupu] Which hupu adapter commands call postHupuJson/buildBrowserJsonPostScript, what apiUrl endpoints and payload bodies do they pass (especially mode='reply'), and are destinations restricted to bbs.hupu.com or can session-derived identifiers reach other hosts?
Data exfiltration
[file request clis/qwen/history.js] Which host does the QIANWEN_API_DOMAIN constant used by getSessionListFromApi resolve to, i.e., where are the utdid cookie value and the session-list POST actually sent?
Data exfiltration
[file request clis/spotify/spotify.js] What are the SCOPES, REDIRECT_URI, ENV_FILE, and TOKEN_FILE constants defined at the top of spotify.js, i.e., which OAuth scopes are requested and which local files hold the client secret and tokens?
Credential access
[file request clis/youtube/utils.js] The SAPISID cookie value read via readYoutubeSapisid is documented to feed a SAPISIDHASH Authorization helper in the unshown lines 186-245; confirm the derived header is only used for youtube.com requests and the raw SAPISID value never leaves the host or reaches a non-YouTube destination.
Credential access
[file request dist/src/convention-audit.js] scripts/check-typed-error-lint.mjs dynamically imports and executes this built module (runConventionAudit); does it perform any network access, writes outside the repository, or execution of externally fetched code?
Code execution
Refuted findings 6

Candidates the review stage examined and rejected, kept for transparency.

Credential access

criticalRefutedmedium confidence

clis/1point3acres/utils.js — cited in the skill docs as the template agents should copy for site-level argument-normalization helpers — also ships getCookie(), which reads HttpOnly Discuz auth/session cookies out of the live browser session for www.1point3acres.com and the root domain .1point3acres.com and returns them to callers for authenticated requests, i.e., browser-store credential access beyond argument normalization, though scoped to the wrapped site and openly documented.

const cookies = await page.getCookies(opts);
Why it was refuted

getCookie is hard-scoped to the wrapped site's own domains and openly documented; reading the wrapped site's HttpOnly session cookies to authenticate requests to that same site is the cookie-strategy adapter capability this skill ships as its material, so the declared-core-capability carve-out applies.

Data exfiltration

criticalRefutedlow confidence

The chatgpt adapter's fetchChatGPTConversationPayload attaches the logged-in browser session's cookie and bearer auth headers (built by buildChatGPTConversationHeaders from the user's ChatGPT session) to node fetch calls to chatgpt.com's backend-api and to in-page credentialed fetches, sending session credentials and retrieving private conversation payloads over the network; the destination is the first-party service the adapter wraps, so this is a real credentialed network chain whose harm depends on it not being the declared wrapped-service capability.

await fetch(`${CHATGPT_URL}/backend-api/conversation/${conversationId}`
Why it was refuted

Fetching the user's own conversation payload from chatgpt.com's backend-api with the user's own ChatGPT session (cookie/bearer) is first-party wrapped-service access — the core capability of the shipped adapter that declares itself ChatGPT web automation helpers; the cross-origin cookie contamination on these headers is separately captured by the standing unfiltered-getCookies findings.

Data exfiltration

criticalRefutedmedium confidence

setChatGPTModelConfig, invoked by selectChatGPTModel as its API fallback, attaches the harvested session cookies and a bearer access token to a PATCH request that mutates the user's ChatGPT account model settings and then writes an oai-last-model-config cookie into the browser.

const response = await fetch(patchUrl, {
Why it was refuted

The PATCH targets the wrapped service's own user_last_used_model_config endpoint as the API fallback of the user-commanded selectChatGPTModel, using the user's own ChatGPT session — first-party model management carved out as the shipped adapter's core capability; the profile-wide cookie sweep inside its headers is already adjudicated by the standing 532/1763 findings.

Credential access

criticalRefutedlow confidence

fetchJobCards reads the LinkedIn JSESSIONID session cookie from the browser cookie store via CDP and injects it as a csrf-token header into authenticated in-page requests to LinkedIn's internal Voyager API, a wrapped-service credential read whose reach is confined to linkedin.com in the shown code.

const cookies = await page.getCookies({ url: 'https://www.linkedin.com' });
Why it was refuted

The cookie read is strictly scoped to https://www.linkedin.com and the JSESSIONID is injected as the csrf-token for the wrapped service's own Voyager API in the same page context — wrapped-service credential use, the cookie-strategy adapter carve-out this skill ships as its material.

Credential access

criticalRefutedlow confidence

The ONES adapter reads ONES_USER_ID and ONES_AUTH_TOKEN from the environment and attaches them as Ones-User-Id/Ones-Auth-Token headers to in-page fetch calls sent with credentials:include to whatever origin the ONES_BASE_URL environment variable names, so the token's sink is entirely env-determined.

const tok = process.env.ONES_AUTH_TOKEN?.trim() || process.env.Ones_Auth_Token?.trim();
Why it was refuted

ONES_AUTH_TOKEN/ONES_USER_ID are the wrapped ONES service's own documented API credentials, attached as Ones-Auth-Token/Ones-User-Id headers to requests built from the ONES deployment's base URL — squarely the env-API-keys-for-the-wrapped-service carve-out, since the env vars and the base URL together configure that same service.

Credential access

highRefutedlow confidence

The LinkedIn adapter extracts the signed-in browser session's JSESSIONID cookie value via page.getCookies and returns it to CLI callers as a reusable session credential, with the shown code placing no restriction on where callers transmit it.

const jsession = cookies.find((c) => c.name === 'JSESSIONID')?.value;
Why it was refuted

requireLinkedInCookie reads only cookies scoped to https://www.linkedin.com to supply the wrapped service's session credential, and no unit shows any sink outside linkedin.com, so the unrestricted-caller claim is an unsupported absence and the shown behavior is the wrapped-service credential carve-out.

Coverage gaps 6

Parts of the artifact the static analysis could not fully examine.

ledger_finding_without_valid_finding run 1: declared-vs-actual.9 SRC:1f0e6d641f3f0018
ledger_finding_without_valid_finding run 1: declared-vs-actual.9 SRC:9bacfb1c0b56aab8
invalid_judge_finding declared-vs-actual.9
request_unanswered request.1: invalid file request (evidence_ids not shown in the asking request)
request_unanswered request.7: request.5/hupu-utils-full: finding: no valid finding cites the answer's evidence
invalid_judge_finding request.7