{"schemaVersion":"1.0","type":"Article","slug":"automating-ai-context-how-i-built-a-custom-extension-for-ibm-bob-ide-to-inject-project-rules-ro4ye","url":"https://api.zyvop.com/automating-ai-context-how-i-built-a-custom-extension-for-ibm-bob-ide-to-inject-project-rules-ro4ye","title":"Automating AI Context: How I Built a Custom Extension for IBM Bob IDE to Inject Project Rules","subtitle":null,"tldr":"Building my own “vsix” extension to automate my own projects Introduction Setting up consistent workspace instructions across projects can quickly become a tedi...","keywords":["bob","sdlc","vsix","IDE"],"entities":["Alain Airom (Ayrom)","Build Engineer","bob","sdlc","vsix","IDE","ZyVOP"],"keyTakeaways":["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."],"headings":["TL;DR-What is AGENTS.MD?","Hooking into the IDE Event Loop","Idempotent Injections &amp; Safety Checks","Dependency-Free Packaging and Installation"],"outboundLinks":["https://bob.ibm.com/"],"contentText":"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.md can 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 ... Enter fullscreen mode Exit fullscreen mode 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) │ └──► &lt;project&gt;/AGENTS.md ← Injected automatically when project opens Enter fullscreen mode Exit fullscreen mode 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 Enter fullscreen mode Exit fullscreen mode 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// &lt;workspaceRoot&gt;/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)// () =&gt; 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(() =&gt; { void injectAgentsMd(); }) ); // ── Trigger 3: settings change — mirror bob.marketplace.installLocation ── context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) =&gt; { if (e.affectsConfiguration('bob.agentsInjector')) { void injectAgentsMd(); } }) );} function deactivate() { /* nothing to clean up */} module.exports = { activate, deactivate }; Enter fullscreen mode Exit fullscreen mode Idempotent Injections &amp; 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.MD exists before taking action. Missing Target: If no AGENTS.md is 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 = `&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt; &lt;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\"&gt; &lt;Metadata&gt; &lt;Identity Language=\"en-US\" Id=\"${PKG.name}\" Version=\"${PKG.version}\" Publisher=\"${PKG.publisher}\"/&gt; &lt;DisplayName&gt;${PKG.displayName}&lt;/DisplayName&gt; &lt;Description&gt;${PKG.description}&lt;/Description&gt; &lt;Tags&gt;bob,agents,automation&lt;/Tags&gt; &lt;GalleryFlags&gt;Public&lt;/GalleryFlags&gt; &lt;License&gt;MIT&lt;/License&gt; &lt;Categories&gt;Other&lt;/Categories&gt; &lt;/Metadata&gt; &lt;Installation&gt; &lt;InstallationTarget Id=\"Microsoft.VisualStudio.Code\" Version=\"[1.85,)\"/&gt; &lt;/Installation&gt; &lt;Dependencies/&gt; &lt;Assets&gt; &lt;Asset Type=\"Microsoft.VisualStudio.Code.Manifest\" Path=\"extension/package.json\" Addressable=\"true\"/&gt; &lt;/Assets&gt; &lt;/PackageManifest&gt;`; // ── [Content_Types].xml ───────────────────────────────────────────────────────const CONTENT_TYPES = `&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt; &lt;Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"&gt; &lt;Default Extension=\"json\" ContentType=\"application/json\"/&gt; &lt;Default Extension=\"js\" ContentType=\"application/javascript\"/&gt; &lt;Default Extension=\"vsixmanifest\" ContentType=\"text/xml\"/&gt; &lt;/Types&gt;`; // ── CRC-32 (standard ZIP polynomial) ─────────────────────────────────────────const CRC_TABLE = (() =&gt; {const t = new Uint32Array(256);for (let n = 0; n &lt; 256; n++) { let c = n; for (let k = 0; k &lt; 8; k++) c = (c &amp; 1) ? (0xedb88320 ^ (c &gt;&gt;&gt; 1)) : (c &gt;&gt;&gt; 1); t[n] = c;}return t;})(); function crc32(buf) {let crc = 0xffffffff;for (let i = 0; i &lt; buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) &amp; 0xff] ^ (crc &gt;&gt;&gt; 8);return (crc ^ 0xffffffff) &gt;&gt;&gt; 0;} // ── DOS date/time for ZIP headers ─────────────────────────────────────────────function dosDateTime(d) {const date = ((d.getFullYear() - 1980) &lt;&lt; 9) | ((d.getMonth() + 1) &lt;&lt; 5) | d.getDate();const time = (d.getHours() &lt;&lt; 11) | (d.getMinutes() &lt;&lt; 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); // signaturelocal.writeUInt16LE(20, 4); // version needed to extract (2.0)local.writeUInt16LE(0, 6); // general purpose bit flaglocal.writeUInt16LE(8, 8); // compression method: deflatelocal.writeUInt16LE(time, 10); // last mod file timelocal.writeUInt16LE(date, 12); // last mod file datelocal.writeUInt32LE(crc, 14); // crc-32local.writeUInt32LE(compressed.length, 18); // compressed sizelocal.writeUInt32LE(rawData.length, 22); // uncompressed sizelocal.writeUInt16LE(nameBytes.length, 26); // file name lengthlocal.writeUInt16LE(0, 28); // extra field lengthnameBytes.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 signaturerec.writeUInt16LE(20, 4); // version made byrec.writeUInt16LE(20, 6); // version neededrec.writeUInt16LE(0, 8); // general purpose bit flagrec.writeUInt16LE(8, 10); // compression method: deflaterec.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 lengthrec.writeUInt16LE(0, 30); // extra field lengthrec.writeUInt16LE(0, 32); // file comment lengthrec.writeUInt16LE(0, 34); // disk number startrec.writeUInt16LE(0, 36); // internal file attributesrec.writeUInt32LE(0, 38); // external file attributesrec.writeUInt32LE(localOffset, 42); // relative offset of local headerentry.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 signatureeocd.writeUInt16LE(0, 4); // disk numbereocd.writeUInt16LE(0, 6); // disk with central direocd.writeUInt16LE(entryCount, 8); // entries on this diskeocd.writeUInt16LE(entryCount, 10); // total entrieseocd.writeUInt32LE(centralSize, 12);eocd.writeUInt32LE(centralOffset, 16);eocd.writeUInt16LE(0, 20); // comment lengthreturn eocd;} // ── Assemble VSIX (= ZIP) ─────────────────────────────────────────────────────function buildVsix() {const now = new Date(); // Files to pack — ORDER matters: [Content_Types].xml and .vsixmanifest FIRSTconst 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(); Enter fullscreen mode Exit fullscreen mode 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 &amp;&amp; bob --install-extension bob-agents-injector-1.0.0.vsix\" }, \"devDependencies\": {}} Enter fullscreen mode Exit fullscreen mode 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 Enter fullscreen mode Exit fullscreen mode 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 → &lt;project&gt;/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: &lt;workspaceRoot&gt;/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) ║ ╚══════════════════════════════════════════════════════════════════════╝ Enter fullscreen mode Exit fullscreen mode 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/","contentHash":"sha256:273eb5cf26f1c328bd5f07bdeb3216da52ae55e952bacc9bd7dc5cf74dccc9d1","authorName":"Alain Airom (Ayrom)","authorUrl":"https://api.zyvop.com/author/alain","authorSameAs":["https://github.com/aairom","https://www.linkedin.com/in/aairom/"],"category":null,"tags":["bob","sdlc","vsix","IDE"],"audience":"Readers researching bob","tone":"Professional, build engineer perspective","readingTimeMinutes":12,"wordCount":2603,"faqs":null,"primaryTopic":"bob","publishedAt":"2026-09-03T11:18:24.453Z","updatedAt":"2026-09-03T11:18:24.453Z","canonicalUrl":"https://dev.to/aairom/automating-ai-context-how-i-built-a-custom-extension-for-ibm-bob-ide-to-inject-project-rules-14ho"}