Building my own “vsix” extension to automate my own projects
Introduction
Setting up consistent workspace instructions across projects can quickly become a tedious chore. In IBM Bob IDE, system prompt behaviors and coding standards are guided by a project-level AGENTS.md file located at the root of your repository. While effective, manually copying a master rules file into every newly created or cloned workspace breaks flow and easily leads to configuration drift.
Note: While
AGENTS.mdcan be configured globally across all your projects, I prefer maintaining project-level files. This allows me to start from a shared foundational rule set while giving each workspace the freedom to include custom, project-specific tweaks.
TL;DR-What is AGENTS.MD?
In a modern Software Development Lifecycle (SDLC), an AGENTS.md file is a machine-readable context and configuration specification checked directly into a project repository to dictate how autonomous AI coding agents (such as Cursor, IBM Bob, Claude Code, or GitHub Copilot) interact with the codebase. Functioning as an authoritative set of runtime instructions loaded into the AI’s prompt context, it explicitly defines executable CLI commands, coding standards, project structure maps, architectural constraints, and operational boundaries—such as distinguishing actions the AI can perform autonomously versus those requiring human approval. By checking AGENTS.md into version control alongside application code, software engineering teams standardize AI behavior across developer environments, prevent AI configuration drift, reduce model-generated bugs, and ensure seamless alignment with project-specific development guidelines.
- Example;
# Project Development Rule
- rule 1
- rule 2
...
To eliminate this manual step, I built bob-agents-injector—a custom extension designed specifically for IBM Bob IDE. Developed directly within Bob using its native capabilities, the extension automatically injects a canonical AGENTS.md file from a central source path (defaulting to ~/Devs/AGENTS.MD) into any workspace as soon as it is opened.
~/Devs/AGENTS.MD ← Master rules (single source of truth)
│
└──► <project>/AGENTS.md ← Injected automatically when project opens
Implementation
Hooking into the IDE Event Loop
The key to seamless injection is to align the extension with VS Code's exact activation lifecycle. So I asked Bob to build the required extension to automate copying the AGENTS.MD from my global "Devs" folder each time I create a new folder for a new project using Bob IDE!
Bob IDE is a fork of VS Code (Microsoft's open-source
Code - OSS).
| VS Code | Bob IDE | ||
|---|---|---|---|
| Source | Microsoft (closed binary, open source core) | IBM fork of Code - OSS
|
|
| Extension marketplace | VS Code Marketplace | IBM's own marketplace + Open VSX | |
| Telemetry | Microsoft | IBM | |
| Binary name | code |
bobide / bobide-insiders
|
|
| Extensions dir | ~/.vscode/extensions |
~/.bobide/extensions |
scripts/
├── bob-agents-injector/
│ ├── package.json
│ ├── out/extension.js
│ └── build.js
└── install-bob-agents-injector.sh ← optional convenience wrapper
onStartupFinished: Fires when Bob completes initial loading.onDidChangeWorkspaceFolders: Fires dynamically whenever a new project folder is opened or swapped.
// =============================================================================
// Bob AGENTS.md Auto-Injector — extension.ts (source, compile with build.js)
//
// This extension mirrors the EXACT trigger chain used by the bob-marketplace
// extension to inject .bob/mcp.json, but instead copies ~/Devs/AGENTS.MD to
// <workspaceRoot>/AGENTS.md at the same moment:
//
// Trigger 1: onStartupFinished → fires when Bob IDE is ready at launch
// Trigger 2: onDidChangeWorkspaceFolders → fires when a NEW project is opened
//
// Both map directly to the two events in bob-marketplace/out/extension.js:
// void mcpServer.start() (at activate time)
// vscode.workspace.onDidChangeWorkspaceFolders( (at workspace change)
// () => void mcpServer.reregister())
//
// Idempotency: if AGENTS.md already exists in the workspace root and its
// content matches the source file, nothing is written. If it exists but
// differs, it is left untouched (user may have customised it).
// =============================================================================
'use strict';
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');
const os = require('os');
// ── Configuration helpers ────────────────────────────────────────────────────
function getConfig() {
const cfg = vscode.workspace.getConfiguration('bob.agentsInjector');
const rawSource = cfg.get('sourcePath', '').trim()
|| path.join(os.homedir(), 'Devs', 'AGENTS.MD');
// Expand leading ~ (vscode settings may contain literal ~)
const sourcePath = rawSource.startsWith('~')
? path.join(os.homedir(), rawSource.slice(1))
: rawSource;
return {
enabled: cfg.get('enabled', true),
sourcePath,
targetFileName: cfg.get('targetFileName', 'AGENTS.md').trim() || 'AGENTS.md',
};
}
// ── Core inject function ─────────────────────────────────────────────────────
/**
* Copies the master AGENTS.MD into the workspace root as AGENTS.md,
* following the same logic as MarketplaceMcpServer.registerIntoWorkspace():
* - reads existing target (non-fatal if missing)
* - skips if content is identical (idempotent)
* - skips if content differs (user may have customised it)
* - creates the target file only when it does not exist
*/
async function injectAgentsMd() {
const cfg = getConfig();
if (!cfg.enabled) {
return;
}
// ── Validate source ───────────────────────────────────────────────────────
if (!fs.existsSync(cfg.sourcePath) || !fs.statSync(cfg.sourcePath).isFile()) {
// Non-fatal: source missing means the user hasn't set up the master file.
// Log quietly — don't pop an annoying dialog on every project open.
console.warn(`[bob-agents-injector] Source not found: ${cfg.sourcePath}. ` +
`Set 'bob.agentsInjector.sourcePath' in settings.`);
return;
}
// ── Resolve workspace root ────────────────────────────────────────────────
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
return; // No workspace open yet — identical to bob-marketplace's behaviour
}
// Mirror bob-marketplace: use first workspace folder (workspaceFolders[0])
const workspaceRoot = folders[0].uri.fsPath;
const targetPath = path.join(workspaceRoot, cfg.targetFileName);
// ── Read source content ────────────────────────────────────────────────────
let sourceContent;
try {
sourceContent = fs.readFileSync(cfg.sourcePath);
} catch (err) {
console.warn(`[bob-agents-injector] Could not read source: ${err.message}`);
return;
}
// ── Check existing target ─────────────────────────────────────────────────
if (fs.existsSync(targetPath)) {
try {
const existing = fs.readFileSync(targetPath);
if (existing.equals(sourceContent)) {
// Already up-to-date — silent no-op (same as bob-marketplace
// behaviour when mcp.json already has the right URL)
return;
} else {
// Target exists and differs — user may have customised it.
// Do NOT overwrite. Log for visibility.
console.log(`[bob-agents-injector] ${cfg.targetFileName} exists and ` +
`differs from master — skipping to preserve local changes.`);
return;
}
} catch {
// Can't read existing file — try writing anyway
}
}
// ── Write target ──────────────────────────────────────────────────────────
try {
fs.writeFileSync(targetPath, sourceContent);
console.log(`[bob-agents-injector] ✅ Injected ${cfg.targetFileName} → ${targetPath}`);
} catch (err) {
console.warn(`[bob-agents-injector] Could not write target: ${err.message}`);
}
}
// ── Extension entry points ───────────────────────────────────────────────────
function activate(context) {
console.log('[bob-agents-injector] Activating (onStartupFinished)');
// ── Trigger 1: at IDE startup — mirror mcpServer.start() call ────────────
void injectAgentsMd();
// ── Trigger 2: when workspace changes — mirror onDidChangeWorkspaceFolders
// This is the KEY trigger: fires when the user opens a NEW project folder
// in Bob, which is the exact same moment .bob/mcp.json is (re-)injected.
context.subscriptions.push(
vscode.workspace.onDidChangeWorkspaceFolders(() => {
void injectAgentsMd();
})
);
// ── Trigger 3: settings change — mirror bob.marketplace.installLocation ──
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('bob.agentsInjector')) {
void injectAgentsMd();
}
})
);
}
function deactivate() {
/* nothing to clean up */
}
module.exports = { activate, deactivate };
Idempotent Injections & Safety Checks
Automatically modifying workspace files requires strict safeguards to avoid overwriting intentionally customized local rules. The injectAgentsMd() execution loop enforces idempotency through a simple decision flow:
Verify Source: Ensures
~/Devs/AGENTS.MDexists before taking action.Missing Target: If no
AGENTS.mdis present at the target workspace root, it creates one from the master file.Identical Content: If the target matches the master file byte-for-byte, execution skips silently.
Modified Target: If the target exists but differs from the master file (indicating local edits), execution skips to preserve project-specific overrides.
Local AGENTS.md State |
Extension Action |
|---|---|
| Does not exist | Copy master AGENTS.MD to workspace root MD+ 2 |
| Exists (identical) | Skip silently (up-to-date) MD+ 2 |
| Exists (modified) | Skip execution (preserve local customizations) MD+ 2 |
Dependency-Free Packaging and Installation
To keep the development footprint minimal, the project packages its VSIX installer using a zero-dependency build.js script reliant solely on Node.js built-ins (fs, path, and zlib). It constructs a valid .vsix archive directly from raw buffers, complete with proper CRC-32 checksums and ZIP structure formatting.
#!/usr/bin/env node
// =============================================================================
// build.js — packages bob-agents-injector into a .vsix file
//
// Usage (run from scripts/bob-agents-injector/):
// node build.js
//
// Then install into Bob:
// /Applications/IBM\ Bob\ -\ Insiders.app/Contents/Resources/app/bin/bobide-insiders \
// --install-extension bob-agents-injector-1.0.0.vsix
//
// No npm install needed — uses only Node.js built-ins.
// =============================================================================
'use strict';
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const DIR = __dirname;
const PKG = JSON.parse(fs.readFileSync(path.join(DIR, 'package.json'), 'utf8'));
const VSIX = path.join(DIR, `${PKG.name}-${PKG.version}.vsix`);
// ── VSIX manifest ─────────────────────────────────────────────────────────────
const VSIX_MANIFEST = `<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0"
xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"
xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Language="en-US"
Id="${PKG.name}"
Version="${PKG.version}"
Publisher="${PKG.publisher}"/>
<DisplayName>${PKG.displayName}</DisplayName>
<Description>${PKG.description}</Description>
<Tags>bob,agents,automation</Tags>
<GalleryFlags>Public</GalleryFlags>
<License>MIT</License>
<Categories>Other</Categories>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Code" Version="[1.85,)"/>
</Installation>
<Dependencies/>
<Assets>
<Asset Type="Microsoft.VisualStudio.Code.Manifest"
Path="extension/package.json"
Addressable="true"/>
</Assets>
</PackageManifest>`;
// ── [Content_Types].xml ───────────────────────────────────────────────────────
const CONTENT_TYPES = `<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="json" ContentType="application/json"/>
<Default Extension="js" ContentType="application/javascript"/>
<Default Extension="vsixmanifest" ContentType="text/xml"/>
</Types>`;
// ── CRC-32 (standard ZIP polynomial) ─────────────────────────────────────────
const CRC_TABLE = (() => {
const t = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
t[n] = c;
}
return t;
})();
function crc32(buf) {
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
return (crc ^ 0xffffffff) >>> 0;
}
// ── DOS date/time for ZIP headers ─────────────────────────────────────────────
function dosDateTime(d) {
const date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
const time = (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2);
return { date, time };
}
// ── Build one ZIP entry (local header + data + central-dir record) ────────────
function makeEntry(name, rawData, now) {
const nameBytes = Buffer.from(name, 'utf8');
const compressed = zlib.deflateRawSync(rawData, { level: 9 });
const crc = crc32(rawData);
const { date, time } = dosDateTime(now);
// Local file header (signature 0x04034b50)
const local = Buffer.alloc(30 + nameBytes.length);
local.writeUInt32LE(0x04034b50, 0); // signature
local.writeUInt16LE(20, 4); // version needed to extract (2.0)
local.writeUInt16LE(0, 6); // general purpose bit flag
local.writeUInt16LE(8, 8); // compression method: deflate
local.writeUInt16LE(time, 10); // last mod file time
local.writeUInt16LE(date, 12); // last mod file date
local.writeUInt32LE(crc, 14); // crc-32
local.writeUInt32LE(compressed.length, 18); // compressed size
local.writeUInt32LE(rawData.length, 22); // uncompressed size
local.writeUInt16LE(nameBytes.length, 26); // file name length
local.writeUInt16LE(0, 28); // extra field length
nameBytes.copy(local, 30);
return {
localBlock: Buffer.concat([local, compressed]),
crc, date, time,
compressedSize: compressed.length,
uncompressedSize: rawData.length,
nameBytes,
};
}
// ── Central directory record for one entry ────────────────────────────────────
function centralRecord(entry, localOffset) {
const rec = Buffer.alloc(46 + entry.nameBytes.length);
rec.writeUInt32LE(0x02014b50, 0); // central dir signature
rec.writeUInt16LE(20, 4); // version made by
rec.writeUInt16LE(20, 6); // version needed
rec.writeUInt16LE(0, 8); // general purpose bit flag
rec.writeUInt16LE(8, 10); // compression method: deflate
rec.writeUInt16LE(entry.time, 12);
rec.writeUInt16LE(entry.date, 14);
rec.writeUInt32LE(entry.crc, 16);
rec.writeUInt32LE(entry.compressedSize, 20);
rec.writeUInt32LE(entry.uncompressedSize, 24);
rec.writeUInt16LE(entry.nameBytes.length, 28); // file name length
rec.writeUInt16LE(0, 30); // extra field length
rec.writeUInt16LE(0, 32); // file comment length
rec.writeUInt16LE(0, 34); // disk number start
rec.writeUInt16LE(0, 36); // internal file attributes
rec.writeUInt32LE(0, 38); // external file attributes
rec.writeUInt32LE(localOffset, 42); // relative offset of local header
entry.nameBytes.copy(rec, 46);
return rec;
}
// ── End of central directory record ──────────────────────────────────────────
function eocdRecord(entryCount, centralSize, centralOffset) {
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0); // end of central dir signature
eocd.writeUInt16LE(0, 4); // disk number
eocd.writeUInt16LE(0, 6); // disk with central dir
eocd.writeUInt16LE(entryCount, 8); // entries on this disk
eocd.writeUInt16LE(entryCount, 10); // total entries
eocd.writeUInt32LE(centralSize, 12);
eocd.writeUInt32LE(centralOffset, 16);
eocd.writeUInt16LE(0, 20); // comment length
return eocd;
}
// ── Assemble VSIX (= ZIP) ─────────────────────────────────────────────────────
function buildVsix() {
const now = new Date();
// Files to pack — ORDER matters: [Content_Types].xml and .vsixmanifest FIRST
const files = [
{ name: '[Content_Types].xml', data: Buffer.from(CONTENT_TYPES, 'utf8') },
{ name: '.vsixmanifest', data: Buffer.from(VSIX_MANIFEST, 'utf8') },
{ name: 'extension/package.json', data: fs.readFileSync(path.join(DIR, 'package.json')) },
{ name: 'extension/out/extension.js', data: fs.readFileSync(path.join(DIR, 'out', 'extension.js')) },
];
const localBlocks = [];
const centralRecs = [];
let offset = 0;
for (const { name, data } of files) {
const entry = makeEntry(name, data, now);
centralRecs.push(centralRecord(entry, offset));
localBlocks.push(entry.localBlock);
offset += entry.localBlock.length;
}
const centralBuf = Buffer.concat(centralRecs);
const eocd = eocdRecord(files.length, centralBuf.length, offset);
fs.writeFileSync(VSIX, Buffer.concat([...localBlocks, centralBuf, eocd]));
const kb = (fs.statSync(VSIX).size / 1024).toFixed(1);
console.log(`✅ Built: ${VSIX} (${kb} KB)`);
console.log('');
console.log('Or install directly with (use the correct variant for your machine):');
console.log(` # Standard IBM Bob:`);
console.log(` /Applications/IBM\\ Bob.app/Contents/Resources/app/bin/bobide --install-extension "${VSIX}"`);
console.log(` # IBM Bob - Insiders:`);
console.log(` /Applications/IBM\\ Bob\\ -\\ Insiders.app/Contents/Resources/app/bin/bobide-insiders --install-extension "${VSIX}"`);
console.log('');
console.log('Or just run: bash scripts/install-bob-agents-injector.sh');
}
buildVsix();
- The dependencies of the code;
{
"name": "bob-agents-injector",
"displayName": "Bob AGENTS.md Auto-Injector",
"description": "Automatically copies ~/Devs/AGENTS.MD into AGENTS.md at the root of every workspace opened in IBM Bob — using the same trigger timing as the bob-marketplace mcp.json injection.",
"version": "1.0.0",
"publisher": "local",
"license": "MIT",
"engines": { "vscode": "^1.85.0" },
"categories": ["Other"],
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "Bob AGENTS.md Injector",
"properties": {
"bob.agentsInjector.sourcePath": {
"type": "string",
"default": "",
"description": "Absolute path to the master AGENTS.MD. Defaults to ~/Devs/AGENTS.MD. Supports ~ expansion."
},
"bob.agentsInjector.targetFileName": {
"type": "string",
"default": "AGENTS.md",
"description": "File name to write in the workspace root. Bob reads 'AGENTS.md' (case-insensitive on macOS)."
},
"bob.agentsInjector.enabled": {
"type": "boolean",
"default": true,
"description": "Enable or disable automatic AGENTS.md injection."
}
}
}
},
"scripts": {
"compile": "node build.js",
"package": "node build.js && bob --install-extension bob-agents-injector-1.0.0.vsix"
},
"devDependencies": {}
}
- The next step is to build the ".vsix" file.
# Package the VSIX without npm dependencies
node build.js
# Install automatically into IBM Bob or IBM Bob Insiders
bash scripts/install-bob-agents-injector.sh
- Once the bash runs, the extension is in place.
╔══════════════════════════════════════════════════════════════════════╗
║ ✅ bob-agents-injector is now installed! ║
╠══════════════════════════════════════════════════════════════════════╣
║ ║
║ How it works: ║
║ • On every project open, Bob fires onDidChangeWorkspaceFolders ║
║ • The injector catches that event (same as bob-marketplace does) ║
║ • It copies ~/Devs/AGENTS.MD → <project>/AGENTS.md ║
║ • If AGENTS.md already exists and is unmodified, it skips it ║
║ • If AGENTS.md was customised locally, it leaves it untouched ║
║ ║
║ Bob's RuleLoader reads: <workspaceRoot>/AGENTS.md ║
║ (NOT .bob/AGENTS.MD — that path is not read by Bob) ║
║ ║
║ Optional settings (Bob Settings → search 'agentsInjector'): ║
║ bob.agentsInjector.sourcePath custom source path ║
║ bob.agentsInjector.targetFileName target file name ║
║ bob.agentsInjector.enabled toggle on/off ║
║ ║
║ To trigger immediately: reload the Bob window ║
║ Menu → Developer → Reload Window (or Cmd+Shift+P → Reload) ║
╚══════════════════════════════════════════════════════════════════════╝
Once installed, user preferences can be adjusted anytime via Bob Settings (Cmd+,) under the bob.agentsInjector namespace to customize target file naming or update source paths.
Conclusion
Automating workspace preparation ensures that every AI interaction starts with consistent context, constraints, and project rules without adding repetitive setup steps. By mirroring IBM Bob’s internal event models, bob-agents-injector bridges the gap between global master rules and individual repository management.
Whether working across stable or Insiders builds of IBM Bob, spending a few minutes to build local IDE utilities pays back continuous micro-dividends in productivity and rule enforcement across every project you touch.
Thanks for reading 🏗️
Links
- IBM Bob: https://bob.ibm.com/




Top comments (0)