Skip to content
HN On Hacker News ↗

Turning VS Code into the control panel for my work

▲ 6 points • 2 comments • by dmonterocrespo • 2w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is AI.

100 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 3
SEGMENTS · AI 3 of 3
WORD COUNT 1,269
PEAK AI % 100% · §1
Analyzed
Oct 27
backend: pangram/v3.3
Segments scanned
3 windows
avg 423 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,269 words · 3 segments analyzed

Human AI-generated
§1 AI · 100%

6 min read3 days ago--Press enter or click to view image in full sizeThe first panel I connected to VS Code had nothing to do with writing code. It showed the Jira tickets assigned to me that week, which until then I was checking in the browser three or four times a day. I put it together in an afternoon, with a Python script I was already running from the terminal and about a hundred lines of TypeScript to draw the list.The others came later.

§2 AI · 100%

Server status.

§3 AI · 100%

Pipeline runs with streaming logs. Several SSH sessions open at once, each tied to a project. Pod and deployment state, with describe and logs one click away. And finally token usage for Claude and Codex, broken down by project.None of those panels is a new tool I had to learn. They are all extensions I wrote, and they all follow the same pattern: a button in the interface runs a script that already existed.What lives in the editor nowThis is the side panel, roughly in the order I added things:Servers. Load, uptime, and disk space per machine, read over SSH by a script that returns JSON.Jira. My assigned tickets, with the available status transitions, without leaving the editor.Pipelines. Start a build and follow its log in a view that refreshes itself.SSH. Several connections in parallel, each bound to a project and with its own terminal.Kubernetes. Pods per namespace, describe for the selected pod, and streaming logs.Tokens. Daily Claude and Codex spend, by project and by model, read from the local session files.Why an extension instead of a web dashboardThe main reason is unglamorous: I am already in the editor. A web dashboard is another tab, another login, another expiring token, another thing that gets forgotten on the second monitor.Two other reasons tipped the decision. The first is context: the extension knows which folder I am in, which branch is active, and which Jira project belongs to this repository. An external dashboard knows none of that and has to be told every time. The second is that I can change it. If I want to see a ticket’s original estimate next to its logged time, I add it and commit, instead of waiting for a SaaS to ship it.How it is put togetherThe pattern is the same across all six extensions, and it is deliberately boring.Press enter or click to view image in full sizeThe extension orchestrates, the script executes. The TypeScript code holds no business logic. It registers commands, draws the view, and spawns processes.The logic lives in Python scripts, outside the extension. I can test a script in the terminal without opening VS Code, reuse it from a cron job or from an agent, and if it breaks the extension does not go down with it.The contract between the two layers is fixed. Arguments or stdin on the way in, JSON shaped as {ok, data, error} on the way out. Errors are shown as they arrive, without reinterpretation. Once the contract is clear, the interface can be thrown away and rebuilt without touching the logic.The webview does not talk to the network. HTML, CSS, and JavaScript with no framework, communicating with the extension through messages. Every external call leaves from the extension process, so there is a single place to look when something fails.A single action travels like this:Press enter or click to view image in full sizeThe code for one command, with nothing extra:tsCopyvscode.commands.registerCommand('panel.jira.myTickets', async () => { const token = await context.secrets.get('jiraToken'); const { stdout } = await execFileAsync('python3', [jiraScript, '--assignee', 'me'], { timeout: 20_000, env: { ...process.env, JIRA_TOKEN: token }, }); const payload = JSON.parse(stdout); if (!payload.ok) { return vscode.window.showErrorMessage(payload.error); } renderTickets(payload.data);});The uncomfortable partVS Code extensions have been an entry point for software supply chain attacks, and it is worth stating precisely how. In October 2025, researchers documented GlassWorm, a worm that spread by publishing infected versions of extensions that already existed and worked fine. It hid its payload in Unicode characters the editor does not render, harvested npm, GitHub, and git credentials, and reached roughly 35,800 compromised installations across Open VSX and the Microsoft Marketplace. Further waves followed through 2026 before its infrastructure was taken down.On top of that, a detail many people miss: an extension does not run in a sandbox. It runs with your permissions, your ~/.ssh, your environment variables, and your git session. The Workspace Trust documentation says so explicitly: Restricted Mode cannot prevent a malicious extension from executing code and ignoring it.So what changes when you write the extension yourself? The nature of the risk changes, not its existence. Your code still runs unsandboxed, and a command injection bug or a compromised dependency produces the same outcome as a malicious extension from the Marketplace. What does change:Silent updates disappear. GlassWorm’s vector was pushing a new version over an extension that was already installed. If you compile yours as a .vsix and install it that way, nobody can publish an update over it.The code is auditable. It sits in your repository, with commit history and review. You can answer what permissions it holds, which dependencies it pulls in, and what happens when it changes.Dependencies can be counted. Many Marketplace extensions drag in dozens of transitive packages. Mine use the VS Code API, child_process, and nothing else.Least privilege becomes enforceable. This command only reads tickets, that one only runs kubectl get. kubectl delete does not exist as a button.Writing your own extensions does not make you immune. It hands control back to you, and control poorly exercised is a comfortable illusion.The rules I followCredentials in SecretStorage, never in settings.json. Settings files get synced, photographed in screenshots, and committed to repositories.execFile with an argument array, never an interpolated shell string. A pod name coming from the Kubernetes API is not trustworthy either.A whitelist of actions in the manifest. There is no field where the user types an arbitrary command.No eval, no downloading and executing code at runtime. Code ships with the extension.An explicit confirmation for anything destructive, naming the affected resource.An audit log line with timestamp, command, parameters, and result for every external action.Pinned dependencies and npm audit in CI, even while there are none.Internal .vsix distribution. Publishing to the Marketplace is a separate decision with its own requirements.How to startIf you want to try this, the minimum path is short and the order matters.Install yo and generator-code, run yo code, and pick New Extension (TypeScript). The scaffold takes a minute.Pick one task that currently forces you into a browser. The first two panels I wrote were “assigned tickets” and “are my services alive”.Write the Python script first and test it in the terminal until it returns the JSON you need.Wire it up with execFile and print the result through showInformationMessage. The webview can wait.Use it for a week. If you use it, add the second command. If you do not, delete it: the cost was one afternoon.Do not start with the polished panel or with Kubernetes. Start with the kubectl get pods you type ten times a day.ClosingWhen an agent generates a script and you run it, nothing records what it did or with which permissions. When that same script becomes a command in a versioned extension, you get an input and output contract, a change history, and a log of what happened. That difference means little on a personal project and a fair amount on a team.The hard part of agent-driven automation is no longer generating it. It is giving it a stable place to live with bounded permissions, and the editor is a reasonable candidate for that place because it is already open.