1<!doctype html> 2<html lang="en"> 3 <!-- xladn0 --> 4 <head> 5 <meta charset="utf-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> 7 <meta name="text-scale" content="scale" /> 8 <meta name="referrer" content="strict-origin-when-cross-origin" /> 9 <meta name="author" content="xladn0" /> 10 11
11<script defer src="https://cloud.umami.is/script.js" data-website-id="b81d5be4-2b31-4ec2-a0ff-871fae0fb8f0"></script>
11 12 13 <!-- Favicon declarations. PNG leads because it's the most universally 14 accepted favicon format across Google Search, Slack, Discord, 15 Twitter, older browsers â anything that indexes favicons. SVG 16 comes second as the scalable vector alternative for modern 17 browsers. AVIF stays as a modern-browser bonus. Apple Touch 18 Icon uses PNG (Apple's strong preference for home-screen 19 shortcuts). Safari's pinned-tab still gets the monochrome SVG 20 via mask-icon. --> 21 <link rel="icon" type="image/png" href="/favicon.png" sizes="any" /> 22 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 23 <link rel="icon" type="image/avif" href="/favicon.avif" sizes="any" /> 24 <link rel="apple-touch-icon" href="/favicon.png" /> 25 <link rel="mask-icon" href="/favicon.svg" color="#00ff22" /> 26 <link rel="manifest" href="/site.webmanifest" /> 27 28 <link rel="alternate" type="application/rss+xml" title="xladn0 notebook" href="/rss.xml" /> 29 <link rel="preconnect" href="https://api.fontshare.com" crossorigin /> 30 <link rel="preconnect" href="https://cdn.fontshare.com" crossorigin /> 31 <link rel="dns-prefetch" href="https://cdn.fontshare.com" /> 32 33 <!-- fetchh from old CMS --> 34 <link rel="preconnect" href="https://framerusercontent.com" crossorigin /> 35 <link rel="dns-prefetch" href="https://framerusercontent.com" /> 36 37 <link 38 rel="preload" 39 as="style" 40 href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900,1,400i,500i,700i,900i&display=swap" 41 crossorigin 42 /> 43 44 <link 45 rel="preload" 46 as="font" 47 type="font/otf" 48 href="/font/Graffiti_1_xladn0-Regular.otf" 49 crossorigin 50 /> 51 52 <link 53 rel="stylesheet" 54 href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900,1,400i,500i,700i,900i&display=swap" 55 /> 56 <!-- KaTeX (math rendering) --> 57 <link 58 rel="stylesheet" 59 href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" 60 integrity="sha384-zh0CIslj+VczCZtlzBcjt5ppRcsAmDnRem7ESsYwWwg3m/OaJ2l4x7YBZl9Kxxir" 61 crossorigin="anonymous" 62 /> 63 64 <!-- Boot screen â inline CSS so it renders on first paint, before any 65 external stylesheet has even started downloading. Hides the FOUC 66 while fonts + the hero GIF + Svelte hydration land. 67 68 There's no vector asterisk here at all â the * is BORN as a 69 Conway's Game of Life pattern. Cells bloom radially from the 70 origin along six arms, then Life rules take over and the mark 71 evolves as a living lattice. Each cell has procedural scale + 72 opacity transitions on birth and death (cubic-out easing), so 73 the whole thing reads as a breathing organism. Dismissal is a 74 plain opacity fade. --> 75 <style> 76 #xboot { 77 position: fixed; 78 inset: 0; 79 z-index: 9999; 80 display: flex; 81 align-items: center; 82 justify-content: center; 83 background: #0a0a0a; 84 opacity: 1; 85 overflow: hidden; 86 transition: opacity 520ms cubic-bezier(0.2, 0.7, 0.2, 1); 87 } 88 #xboot.xboot-out { 89 opacity: 0; 90 pointer-events: none; 91 } 92 93 /* Conway's Game of Life canvas. Sized dramatically so it 94 * dominates the boot â it IS the mark. Soft accent-green 95 * drop-shadow gives the whole lattice a luminous glow. */ 96 #xboot .xb-life { 97 width: clamp(20rem, 60vmin, 40rem); 98 height: clamp(20rem, 60vmin, 40rem); 99 display: block; 100 opacity: 0; 101 transition: opacity 700ms cubic-bezier(0.2, 0.7, 0.2, 1); 102 pointer-events: none; 103 image-rendering: pixelated; 104 filter: drop-shadow(0 0 14px rgba(0, 255, 34, 0.35)); 105 } 106 #xboot .xb-life.on { 107 opacity: 1; 108 } 109 </style> 110 111 <link href="../../app/immutable/entry/start.CyoIDKLS.js" rel="modulepreload"> 112 <link href="../../app/immutable/chunks/B3A5AmxR.js" rel="modulepreload"> 113 <link href="../../app/immutable/chunks/DoEoh6Cm.js" rel="modulepreload"> 114 <link href="../../app/immutable/chunks/DhlCwaRs.js" rel="modulepreload"> 115 <link href="../../app/immutable/chunks/BNLWd-Ug.js" rel="modulepreload"> 116 <link href="../../app/immutable/chunks/OA8UcISh.js" rel="modulepreload"> 117 <link href="../../app/immutable/chunks/D-SN2wVR.js" rel="modulepreload"> 118 <link href="../../app/immutable/entry/app.CLUXOrxk.js" rel="modulepreload"> 119 <link href="../../app/immutable/chunks/PPVm8Dsz.js" rel="modulepreload"> 120 <link href="../../app/immutable/chunks/DsnmJJEf.js" rel="modulepreload"> 121 <link href="../../app/immutable/chunks/CmAvehoP.js" rel="modulepreload"> 122 <link href="../../app/immutable/chunks/CFgAmjSS.js" rel="modulepreload"> 123 <link href="../../app/immutable/chunks/vwo1H8Qw.js" rel="modulepreload"> 124 <link href="../../app/immutable/chunks/D3fGjQu7.js" rel="modulepreload"> 125 <link href="../../app/immutable/nodes/0.8kdlNEjn.js" rel="modulepreload"> 126 <link href="../../app/immutable/chunks/MI0V5-Eu.js" rel="modulepreload"> 127 <link href="../../app/immutable/chunks/DeZcLJqm.js" rel="modulepreload"> 128 <link href="../../app/immutable/chunks/DX6gwQ7Y.js" rel="modulepreload"> 129 <link href="../../app/immutable/chunks/EOuKB5DK.js" rel="modulepreload"> 130 <link href="../../app/immutable/chunks/QLtX2_fi.js" rel="modulepreload"> 131 <link href="../../app/immutable/chunks/D_g_GZtA.js" rel="modulepreload"> 132 <link href="../../app/immutable/chunks/M_R7HlNy.js" rel="modulepreload"> 133 <link href="../../app/immutable/chunks/Bo5W785R.js" rel="modulepreload"> 134 <link href="../../app/immutable/chunks/BhzzhRL5.js" rel="modulepreload"> 135 <link href="../../app/immutable/nodes/10.tkxdhSn5.js" rel="modulepreload"> 136 <link href="../../app/immutable/chunks/BheWnx7M.js" rel="modulepreload"> 137 <link href="../../app/immutable/chunks/GzlLhF5P.js" rel="modulepreload"> 138 <link href="../../app/immutable/chunks/D4--IJDw.js" rel="modulepreload"> 139 <link href="../../app/immutable/chunks/CTTOZQt0.js" rel="modulepreload"> 140 <link href="../../app/immutable/chunks/P8DCbwsQ.js" rel="modulepreload"> 141 <link href="../../app/immutable/chunks/BBvijVEp.js" rel="modulepreload"> 142 <link href="../../app/immutable/chunks/Dzl6JGhU.js" rel="modulepreload"> 143 <!--12qhfyh--><meta name="description" content="xladn0 â maker, creator. Software engineering, creative coding, mixed media and digital art, hacking, robotics."/> <meta name="keywords" content="xladn0, Andrej AlfireviÄ, creative coding, digital art, cybersecurity, robotics, generative art, portfolio"/> <link rel="canonical" href="https://xladn0.com/projects/claude-defender/"/> <link rel="author" href="mailto:[email protected]"/> <meta property="og:title" content="xladn0"/> <meta property="og:description" content="xladn0 â maker, creator. Software engineering, creative coding, mixed media and digital art, hacking, robotics."/> <meta property="og:type" content="website"/> <meta property="og:site_name" content="xladn0"/> <meta property="og:url" content="https://xladn0.com/projects/claude-defender/"/> <meta property="og:image" content="https://xladn0.com/og.png"/>
143 <meta property="og:image:width" content="1200"/> <meta property="og:image:height" content="630"/> <meta property="og:image:alt" content="xladn0 â maker, creator"/> <meta property="og:locale" content="en_US"/> <meta name="twitter:card" content="summary_large_image"/> <meta name="twitter:title" content="xladn0"/> <meta name="twitter:description" content="xladn0 â maker, creator. Software engineering, creative coding, mixed media and digital art, hacking, robotics."/> <meta name="twitter:image" content="https://xladn0.com/og.png"/> <meta name="twitter:image:alt" content="xladn0 â maker, creator"/> <meta name="twitter:creator" content="@xladn0"/> <meta name="theme-color" content="#0a0a0a"/> <meta name="color-scheme" content="dark"/> <meta name="format-detection" content="telephone=no"/> <!---->
143<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"Person","@id":"https://xladn0.com/#person","name":"Andrej AlfireviÄ","alternateName":"xladn0","url":"https://xladn0.com","email":"mailto:[email protected]","jobTitle":"Maker, Creator","description":"xladn0 â maker, creator. Software engineering, creative coding, mixed media and digital art, hacking, robotics.","sameAs":["https://github.com/2alf","https://xladn0.itch.io/","https://openprocessing.org/@u435103"]},{"@type":"WebSite","@id":"https://xladn0.com/#website","url":"https://xladn0.com","name":"xladn0","description":"xladn0 â maker, creator. Software engineering, creative coding, mixed media and digital art, hacking, robotics.","inLanguage":"en","author":{"@id":"https://xladn0.com/#person"},"potentialAction":{"@type":"SearchAction","target":"https://xladn0.com/search/?q={search_term_string}","query-input":"required name=search_term_string"}}]}</script>
143<!----><!----><!--gygcht--><!--[0--><meta name="description" content="Securing MCP servers against LotL abuse"/> <meta property="og:description" content="Securing MCP servers against LotL abuse"/> <meta name="twitter:description" content="Securing MCP servers against LotL abuse"/><!--]--> <link rel="canonical" href="https://xladn0.com/projects/claude-defender/"/> <meta property="og:title" content="Heimdall â xladn0"/> <meta property="og:type" content="article"/> <meta property="og:url" content="https://xladn0.com/projects/claude-defender/"/> <meta property="og:image" content="https://xladn0.com/images/defender-claude/logo.png"/> <meta property="og:site_name" content="xladn0"/> <meta property="og:locale" content="en_US"/> <!--[0--><meta property="article:published_time" content="2025-12-31T00:00:00.000Z"/><!--]--> <!--[0--><!--[--><meta property="article:tag" content="hacking"/><meta property="article:tag" content="code"/><meta property="article:tag" content="research"/><!--]--> <meta name="keywords" content="hacking, code, research"/><!--]--> <meta name="twitter:card" content="summary_large_image"/> <meta name="twitter:title" content="Heimdall â xladn0"/> <meta name="twitter:image" content="https://xladn0.com/images/defender-claude/logo.png"/><!----><title>Heimdall â xladn0</title> 144 <link href="../../app/immutable/assets/0.DxMnE591.css" rel="stylesheet"> 145 <link href="../../app/immutable/assets/Grid.izieQIol.css" rel="stylesheet"> 146 <link href="../../app/immutable/assets/Tile.C8SbwH9j.css" rel="stylesheet"> 147 <link href="../../app/immutable/assets/SmoothImage.Ci_0dI0d.css" rel="stylesheet"> 148 <link href="../../app/immutable/assets/10.CE8LDe8I.css" rel="stylesheet"> 149 </head> 150 <body data-sveltekit-preload-data="hover"> 151 <div id="xboot" aria-hidden="true"> 152 <!-- The * is born as a Conway pattern. Cells bloom radially from 153 the origin along six arms, then Life rules take over. Higher 154 native resolution than display size so the downscale stays 155 crisp on high-DPI screens. --> 156 <canvas class="xb-life" width="720" height="720"></canvas> 157 </div> 158 159
159<script> 160 // Boot dismissal gate. Runs inline so it starts before any module 161 // JS is even parsed. The boot screen stays up until ALL of these 162 // resolve: 163 // 164 // 1. document.fonts.ready 165 // â Fontshare Satoshi CSS + self-hosted Graffiti .otf have 166 // both finished downloading and are ready for the browser 167 // to paint with. Without this the site flashes in with 168 // fallback fonts and reflows. 169 // 170 // 2. window.load 171 // â All initial-HTML resources (CSS, JS chunks, IMG tags 172 // emitted by SvelteKit's prerender, KaTeX CSS) have 173 // finished. This includes the hero GIF's byte-fetch but 174 // NOT its decode into a paintable bitmap. 175 // 176 // 3. criticalAssets() 177 // â Explicit tracking of above-the-fold images with an 178 // img.decode() await, so the browser has actually turned 179 // the bytes into a bitmap before we hand off. Without 180 // this the boot can dismiss onto a page where a big 181 // animated GIF is still decoding and paints in a beat 182 // after the fade. 183 // 184 // 4. FULL_CYCLE_MS 185 // â A UX floor so the asterisk animation always gets to 186 // finish its cascade before dismissal, even on hot cache. 187 // 188 // If any of those hang, a HARD_FALLBACK_MS timeout dismisses the 189 // screen anyway. This is longer than in the past (8s vs 5s) so 190 // slow connections get to actually see the fully-loaded page 191 // before the fallback fires â matters mostly on 3G / poor Wi-Fi. 192 (function () { 193 var el = document.getElementById('xboot'); 194 if (!el) return; 195 196 // Reveal bloom (~24 rings à 42ms) + one birth duration + a 197 // beat of settled Life = ~2300ms. Boot never dismisses 198 // before this, so the * always fully forms before handoff. 199 var FULL_CYCLE_MS = 2300; 200 var HARD_FALLBACK_MS = 8000; // last-resort dismissal if something hangs 201 var startedAt = Date.now(); 202 203 // Above-the-fold assets that the boot should explicitly wait 204 // on (in addition to window.load). Keyed by pathname so we 205 // don't spend bandwidth fetching a home-page hero when the 206 // user landed on /projects/foo/. Add entries here as more 207 // pages grow expensive above-the-fold content. 208 var CRITICALS_BY_PATH = { 209 '/': ['/gifs/hero.gif'], 210 '': ['/gifs/hero.gif'] 211 }; 212 213 function fontsReady() { 214 return document.fonts && document.fonts.ready 215 ? document.fonts.ready 216 : Promise.resolve(); 217 } 218 219 function windowLoaded() { 220 return new Promise(function (resolve) { 221 if (document.readyState === 'complete') return resolve(); 222 window.addEventListener('load', resolve, { once: true }); 223 }); 224 } 225 226 function trackImage(src) { 227 // Load + decode. Falls back to load-only if img.decode isn't 228 // available (older browsers). Always resolves â a failed 229 // image shouldn't block dismissal forever. 230 return new Promise(function (resolve) { 231 var img = new Image(); 232 img.fetchPriority = 'high'; 233 img.decoding = 'async'; 234 img.onload = function () { 235 if (img.decode) { 236 img.decode().then(resolve, resolve); 237 } else { 238 resolve(); 239 } 240 }; 241 img.onerror = resolve; 242 img.src = src; 243 }); 244 } 245 246 function criticalAssets() { 247 var path = location.pathname || '/'; 248 var srcs = CRITICALS_BY_PATH[path] || []; 249 if (!srcs.length) return Promise.resolve(); 250 return Promise.all(srcs.map(trackImage)); 251 } 252 253 function tasks() { 254 return Promise.all([ 255 fontsReady(), 256 windowLoaded(), 257 criticalAssets() 258 ]); 259 } 260 261 var dismissed = false; 262 function hide() { 263 if (dismissed) return; 264 dismissed = true; 265 var waited = Date.now() - startedAt; 266 var rest = Math.max(0, FULL_CYCLE_MS - waited); 267 setTimeout(function () { 268 el.classList.add('xboot-out'); 269 setTimeout(function () { 270 el.parentNode && el.parentNode.removeChild(el); 271 }, 600); 272 }, rest); 273 } 274 275 setTimeout(hide, HARD_FALLBACK_MS); 276 tasks().then(hide).catch(hide); 277 })(); 278
279 // Conway-styled asterisk. The * is born entirely from Conway 280 // cells â no vector spokes anywhere. Starts immediately on load 281 // and unfolds the asterisk RADIALLY from the origin outward: 282 // each seeded cell births at a delay proportional to its 283 // Euclidean distance from center, so the pattern blooms 284 // cell-by-cell instead of appearing all at once. Once every 285 // seed cell is born (reveal ends ~1000ms), Conway rules take 286 // over and the mark starts evolving as a Game of Life pattern. 287 // Same procedural scale + opacity per-cell transitions on every 288 // birth/death, cubic-out easing. 289 (function () { 290 var el = document.getElementById('xboot'); 291 var canvas = el && el.querySelector('.xb-life'); 292 if (!canvas || !canvas.getContext) return; 293 if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; 294 295 var ctx = canvas.getContext('2d'); 296 var W = 60, H = 60; 297 var cellPx = canvas.width / W; 298 var COLORS = ['#00ff22', '#8636ff']; 299 var VIOLET_CHANCE = 0.12; 300 var BIRTH_MS = 420; // scale-in + fade-in duration per cell 301 var REVEAL_STEP_MS = 42; // ms between successive-radius reveals 302 var STEP_MS = 560; // Conway step cadence (after reveal ends) 303 var MAX_FILL = 0.82; // cell fill % (leaves grid gaps) 304 305 // Per-cell state. bornAt/diedAt are performance.now() stamps 306 // (>0 == set; 0 == not yet). alive is 0/1. 307 var alive = new Uint8Array(W * H); 308 var scratch = new Uint8Array(W * H); 309 var colors = new Uint8Array(W * H); 310 var bornAt = new Float64Array(W * H); 311 var diedAt = new Float64Array(W * H); 312 313 var cx = W >> 1, cy = H >> 1; 314 315 // Bresenham line into a set of {idx, r} entries â r used for 316 // radial reveal delay. 317 function collect(x0, y0, x1, y1, out) { 318 var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1; 319 var dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1; 320 var err = dx + dy; 321 while (true) { 322 if (x0 >= 0 && x0 < W && y0 >= 0 && y0 < H) { 323 var r = Math.hypot(x0 - cx, y0 - cy); 324 out.push({ idx: y0 * W + x0, r: r }); 325 } 326 if (x0 === x1 && y0 === y1) break; 327 var e2 = 2 * err; 328 if (e2 >= dy) { err += dy; x0 += sx; } 329 if (e2 <= dx) { err += dx; y0 += sy; } 330 } 331 } 332 333 // Six asterisk arm tips at (0, ±24), (±21, ±12) â matches the 334 // SVG asterisk's six-angle 60°-increment geometry mapped into 335 // the 60Ã60 grid. Radius 24 = 40% of grid width, so arms 336 // span most of the canvas but leave room for Life spillover. 337 var tips = [ 338 [cx, cy - 24], 339 [cx + 21, cy - 12], 340 [cx + 21, cy + 12], 341 [cx, cy + 24], 342 [cx - 21, cy + 12], 343 [cx - 21, cy - 12] 344 ]; 345 346 // Collect every seed cell along the arms, dedupe (arms share 347 // the origin), then sort by radial distance so we can birth 348 // them from center outward. 349 var seen = new Set(); 350 var seeds = []; 351 function push(idx, r) { 352 if (seen.has(idx)) return; 353 seen.add(idx); 354 seeds.push({ idx: idx, r: r }); 355 } 356 push(cy * W + cx, 0); 357 for (var t = 0; t < tips.length; t++) { 358 var raw = []; 359 collect(cx, cy, tips[t][0], tips[t][1], raw); 360 for (var i = 0; i < raw.length; i++) push(raw[i].idx, raw[i].r); 361 } 362 seeds.sort(function (a, b) { return a.r - b.r; }); 363 364 // Reveal schedule: assign each seed a birth time offset from 365 // t=0 based on its radius. Cells at the same radius birth 366 // together, giving a clean concentric wave outward. 367 var revealStart = performance.now(); 368 for (var i = 0; i < seeds.length; i++) { 369 // Group by rounded radius so multiple cells at the same 370 // ring birth on the same frame â reads as a coordinated 371 // wave rather than a lazy random spray. 372 var delay = Math.round(seeds[i].r) * REVEAL_STEP_MS; 373 // Store the intended birth timestamp; render will 374 // activate the cell when now >= this value. 375 bornAt[seeds[i].idx] = revealStart + delay; 376 colors[seeds[i].idx] = 377 Math.random() < VIOLET_CHANCE ? 1 : 0; 378 } 379 var revealEndsAt = 380 revealStart + Math.round(seeds[seeds.length - 1].r) * REVEAL_STEP_MS; 381 var conwayStartsAt = revealEndsAt + BIRTH_MS + 240; 382 var lastStepAt = 0; 383 384 385 function ease(t) { 386 // Ease-out cubic â natural for both scale-in and scale-out. 387 var inv = 1 - t; 388 return 1 - inv * inv * inv; 389 } 390 391 function countNeighbors(x, y) { 392 var c = 0; 393 for (var dy = -1; dy <= 1; dy++) {
394 for (var dx = -1; dx <= 1; dx++) { 395 if (dx === 0 && dy === 0) continue; 396 var nx = x + dx, ny = y + dy; 397 if (nx >= 0 && nx < W && ny >= 0 && ny < H) { 398 c += alive[ny * W + nx]; 399 } 400 } 401 } 402 return c; 403 } 404 405 function conwayStep(now) { 406 for (var y = 0; y < H; y++) { 407 for (var x = 0; x < W; x++) { 408 var c = countNeighbors(x, y); 409 var a = alive[y * W + x]; 410 scratch[y * W + x] = 411 a ? (c === 2 || c === 3 ? 1 : 0) : (c === 3 ? 1 : 0); 412 } 413 } 414 for (var i = 0; i < alive.length; i++) { 415 var was = alive[i]; 416 var is = scratch[i]; 417 if (is && !was) { 418 alive[i] = 1; 419 bornAt[i] = now; 420 diedAt[i] = 0; 421 colors[i] = Math.random() < VIOLET_CHANCE ? 1 : 0; 422 } else if (!is && was) { 423 alive[i] = 0; 424 diedAt[i] = now; 425 } 426 } 427 } 428 429 function render(now) { 430 // Reveal phase: activate seed cells whose bornAt has 431 // arrived. Setting alive[i]=1 makes them visible + 432 // eligible for Conway; the bornAt timestamp drives the 433 // scale-in animation via the render pass below. 434 if (now < conwayStartsAt) { 435 for (var i = 0; i < seeds.length; i++) { 436 var s = seeds[i]; 437 if (alive[s.idx] === 0 && now >= bornAt[s.idx]) { 438 alive[s.idx] = 1; 439 } 440 } 441 } else if (now - lastStepAt >= STEP_MS) { 442 // Conway phase: advance the pattern every STEP_MS. 443 conwayStep(now); 444 lastStepAt = now; 445 } 446 447 ctx.clearRect(0, 0, canvas.width, canvas.height); 448 var maxPx = cellPx * MAX_FILL; 449 for (var i = 0; i < W * H; i++) { 450 var born = bornAt[i]; 451 var died = diedAt[i]; 452 var size = 0; 453 var colorIdx = colors[i]; 454 // Dying: still visible but scaling out. Once fully 455 // gone, reset the slot so nothing renders. 456 if (died > 0) { 457 var deathP = (now - died) / BIRTH_MS; 458 if (deathP >= 1) { 459 bornAt[i] = 0; 460 diedAt[i] = 0; 461 continue; 462 } 463 size = 1 - ease(deathP); 464 } else if (alive[i] && born > 0) { 465 var age = now - born; 466 size = age >= BIRTH_MS ? 1 : ease(age / BIRTH_MS); 467 } else if (born > 0 && !alive[i]) { 468 // Seed scheduled but not yet activated â hold at 0. 469 continue; 470 } else { 471 continue; 472 } 473 if (size < 0.04) continue; 474 var px = maxPx * size; 475 var offset = (cellPx - px) / 2; 476 var x = i % W; 477 var y = (i / W) | 0; 478 ctx.globalAlpha = size; 479 ctx.fillStyle = COLORS[colorIdx]; 480 ctx.fillRect(x * cellPx + offset, y * cellPx + offset, px, px); 481 } 482 ctx.globalAlpha = 1; 483 } 484 485 canvas.classList.add('on'); 486 487 var raf; 488 function frame(now) { 489 if (el.classList.contains('xboot-out')) return; 490 render(now); 491 raf = requestAnimationFrame(frame); 492 } 493 raf = requestAnimationFrame(frame); 494 })(); 495 496 // Little easter egg for anyone poking around in DevTools. 497 // Pointer to /security.txt so a curious visitor who finds a real 498 // bug knows how to report it responsibly. 499 console.log( 500 "%cCaught in the act! ", 501 "color: #00ff22; font-weight: bold; font-size: 24px;" 502 ); 503 console.log( 504 "%cYou can stay just dont break anything please, but if you do find something check /security.txt", 505 "font-size: 18px;" 506 ); 507 </script>
507 508 509 <div style="display: contents"><!--[--><!--[0--><!--[--><div class="dock svelte-1h32yp1 floating"><div class="dock-shell svelte-1h32yp1"><!--[-1--><!--]--> <button class="seg seg-primary seg-trigger trigger-iconic svelte-1h32yp1" aria-controls="overlay-menu" aria-expanded="false" aria-label="open menu" title="press M"><span class="trigger-burger svelte-1h32yp1" aria-hidden="true"><span class="svelte-1h32yp1"></span><span class="svelte-1h32yp1"></span><span class="svelte-1h32yp1"></span></span></button> <!--[0--><button class="seg seg-icon seg-music svelte-1h32yp1" aria-pressed="false" aria-label="unmute music" title="unmute"><span class="note svelte-1h32yp1" aria-hidden="true">âª</span></button><!--]--></div></div> <!--[-1--><!--]--> <!--[-1--><!--]--><!----> <!--[-1--><!--]--><!----> <canvas aria-hidden="true" class="svelte-1sgpos4"></canvas><!----> <canvas aria-hidden="true" class="svelte-wj3fob"></canvas><!----> <main class="svelte-12qhfyh"><!----><div class="page-shell"><!--[--><article class="entry svelte-gygcht"><header class="hero container svelte-gygcht"><div class="aster-mark svelte-gygcht" aria-hidden="true">*</div> <div class="hero-top svelte-gygcht"><div class="hero-text svelte-gygcht"><h1 class="svelte-gygcht">Heimdall</h1> <!--[0--><p class="lede svelte-gygcht">Securing MCP servers against LotL abuse</p><!--]--> <dl class="attrs svelte-gygcht"><!--[0--><div class="pair svelte-gygcht"><dt class="svelte-gygcht">year</dt><dd class="svelte-gygcht">2025</dd></div><!--]--> <!--[0--><div class="pair svelte-gygcht"><dt class="svelte-gygcht">w/</dt><dd class="svelte-gygcht">ITU & Anthropic Bug Bounty</dd></div><!--]--> <!--[0--><div class="pair svelte-gygcht"><dt class="svelte-gygcht">role</dt><dd class="svelte-gygcht">ethical hacker, researcher, developer</dd></div><!--]--> <!--[0--><div class="pair svelte-gygcht"><dt class="svelte-gygcht">at</dt><dd class="svelte-gygcht">ITU · Copenhagen</dd></div><!--]--> <!--[0--><div class="pair svelte-gygcht"><dt class="svelte-gygcht">link</dt><dd class="svelte-gygcht"><a href="https://github.com/2alf/Claude-Defender" target="_blank" rel="noopener noreferrer" class="svelte-gygcht">visit â</a></dd></div><!--]--></dl> <!--[-1--><!--]--></div> <!--[0--><div class="hero-graph svelte-gygcht"><!--[0--><svg viewBox="0 0 250 250" class="bloom svelte-1le4ays" aria-hidden="true" style="--w: 250px"><circle cx="125" cy="125" r="80" class="ring svelte-1le4ays"></circle><!--[--><polyline points="125.00,121.80 124.96,118.31 124.76,114.82 124.47,111.33 124.19,107.84 124.05,104.35 124.14,100.85 124.52,97.36 125.14,93.87 125.91,90.38 126.66,86.89 127.20,83.40 126.97,79.91 126.41,76.42 125.71,72.93 125.02,69.44 124.50,65.95 124.24,62.45 124.22,58.96 124.40,55.47 124.66,51.98 124.89,48.49 125.00,45.00" class="line svelte-1le4ays" style="--i: 0"></polyline><polyline points="128.04,124.01 129.61,123.43 131.13,122.68 132.62,121.85 134.13,121.09 135.71,120.55 137.40,120.32 139.18,120.39 141.02,120.66 142.87,120.92 144.64,120.96 146.27,120.56 147.62,119.30 148.91,117.86 150.22,116.47 151.61,115.35 153.12,114.58 154.74,114.15 156.43,113.96 158.15,113.83 159.84,113.62 161.48,113.23 163.04,112.64" class="line svelte-1le4ays" style="--i: 1"></polyline><polyline points="126.88,127.59 127.55,128.72 128.14,129.91 128.81,131.04 129.65,132.05 130.71,132.90 131.93,133.63 133.14,134.37 134.15,135.26 134.79,136.40 135.04,137.85 134.99,139.49 135.12,141.02 135.66,142.25 136.61,143.17 137.84,143.90 139.13,144.58 140.29,145.36 141.22,146.30 141.92,147.41 142.50,148.61 143.09,149.79 143.81,150.89" class="line svelte-1le4ays" style="--i: 2"></polyline><polyline points="123.12,127.59 121.59,129.89 120.12,132.24 118.69,134.62 117.23,136.97 115.70,139.28 114.06,141.50 112.29,143.63 110.37,145.66 108.34,147.60 106.23,149.48 104.10,151.35 102.29,153.45 100.66,155.68 99.18,158.02 97.82,160.45 96.50,162.91 95.18,165.36 93.81,167.78 92.35,170.14 90.78,172.42 89.12,174.62 87.38,176.78" class="line svelte-1le4ays" style="--i: 3"></polyline><polyline points="121.96,124.01 119.28,123.30 116.59,122.63 113.92,121.90 111.29,121.05 108.72,120.03 106.21,118.83 103.73,117.51 101.26,116.16 98.76,114.91 96.19,113.87 93.53,113.13 90.70,112.89 87.86,112.70 85.
50904,112.41 82.29,111.93 79.62,111.22 77.01,110.31 74.45,109.24 71.91,108.13 69.35,107.04 66.76,106.07 64.13,105.22" class="line svelte-1le4ays" style="--i: 4"></polyline><!--]--><!--[--><circle cx="125.00" cy="45.00" class="tip svelte-1le4ays" style="--i: 0"></circle><circle cx="163.04" cy="112.64" class="tip svelte-1le4ays" style="--i: 1"></circle><circle cx="143.81" cy="150.89" class="tip svelte-1le4ays" style="--i: 2"></circle><circle cx="87.38" cy="176.78" class="tip svelte-1le4ays" style="--i: 3"></circle><circle cx="64.13" cy="105.22" class="tip svelte-1le4ays" style="--i: 4"></circle><!--]--><circle cx="125" cy="125" r="1.4" class="origin svelte-1le4ays"></circle><!--[--><text x="125" y="18" text-anchor="middle" dominant-baseline="middle" class="label svelte-1le4ays" style="--i: 0"><tspan class="label-name svelte-1le4ays">code</tspan></text><text x="226.7630472435814" y="91.93518160188063" text-anchor="start" dominant-baseline="middle" class="label svelte-1le4ays" style="--i: 1"><tspan class="label-name svelte-1le4ays">design</tspan></text><text x="187.89302199529462" y="211.56481839811937" text-anchor="start" dominant-baseline="middle" class="label svelte-1le4ays" style="--i: 2"><tspan class="label-name svelte-1le4ays">writing</tspan></text><text x="62.10697800470539" y="211.56481839811937" text-anchor="end" dominant-baseline="middle" class="label svelte-1le4ays" style="--i: 3"><tspan class="label-name svelte-1le4ays">researching</tspan></text><text x="23.23695275641856" y="91.93518160188064" text-anchor="end" dominant-baseline="middle" class="label svelte-1le4ays" style="--i: 4"><tspan class="label-name svelte-1le4ays">hacking</tspan></text><!--]--></svg><!--]--><!----></div><!--]--></div></header> <div class="body container svelte-gygcht"><div class="prose svelte-gygcht"><!--[--><!--[--><div class="split svelte-1qwkpf0" style="--split-cols: 1fr 1fr; --split-gap: 2rem; --split-align: start; margin-top: 1.5em; margin-bottom: 1.5em; "><div style="margin-top:-50px;"><figure class="spot svelte-1y2zl5s" style="--blend: exclusion; "><img src="/images/defender-claude/logo.png" alt="" class="ci svelte-1j6oeeb" loading="lazy" fetchpriority="auto" decoding="async" draggable="false" onload="this.__e=event" onerror="this.__e=event"/><!----> <!--[-1--><!--]--> <!--[0--><figcaption class="cap svelte-1y2zl5s"> </figcaption><!--]--></figure><!----></div> <div align="justify"><p>Heimdall (formerly Claude Defender) grew out of my security research into the Model Context Protocol (MCP) ecosystem. During this work, I identified multiple vulnerabilities in Claude Desktopâs MCP implementation, demonstrating how Claude Desktop could be abused as a Living off the Land (LotL) execution vector, leading to remote code execution, and how modified MCP servers could remain trusted through tool poisoning. I responsibly disclosed the findings to Anthropic, resulting in one duplicate report and one informative report, where the behaviour was acknowledged but considered outside their threat model. The research formed the basis of my university dissertation, which received the highest possible grade.</p> <p>Around the same time, the tool poisoning attack pattern had not yet been formally published. As part of this research, I independently explored the technique and later built Heimdall as a practical mitigation. Initially developed for Claude Desktop, the project has since evolved into a cross-platform integrity monitor for the wider MCP ecosystem, supporting Claude Desktop, Claude Code, Cursor, Windsurf, LM Studio, and custom MCP configurations.</p></div><!----></div><!----> <div class="wide svelte-1wffbbd" style="text-align: left; max-width: none; margin-top: 1.75em; margin-bottom: 1.75em; "><figure class="spot svelte-1y2zl5s" style="--blend: exclusion; "><img src="/images/defender-claude/mm.png" alt="" class="ci svelte-1j6oeeb" loading="lazy" fetchpriority="auto" decoding="async" draggable="false" onload="this.__e=event" onerror="this.__e=event"/><!----> <!--[-1--><!--]--> <!--[0--><figcaption class="cap svelte-1y2zl5s"> </figcaption><!--]--></figure><!----></div><!----> <div class="split svelte-1qwkpf0" style="--split-cols: 1fr 1fr; --split-gap: 2rem; --split-align: start; margin-top: 1.5em; margin-bottom: 1.5em; "><div align="justify"><p>Rather than attempting to detect malicious behaviour directly, Heimdall focuses on ensuring that trusted MCP servers remain trustworthy. It continuously monitors the files AI agents rely on, providing immediate visibility whenever an MCP configuration or server changes unexpectedly.</p> <p>A key challenge for integrity monitoring is protecting the trusted baseline itself. Heimdall addresses this through trustroot, a cryptographically authenticated trust store that signs every integrity record using a keyed BLAKE3 MAC. The signing key is stored inside the operating systemâs secure credential store rather than on disk, making it significantly more difficult for an attacker with only file-write access to silently replace both the monitored files and the baseline used to verify them.</p></div> <div style="margin-top:-50px;"><figure class="spot svelte-1y2zl5s" style="--blend: exclusion; "><img src="/images/defender-claude/ss2.PNG" alt="" class="ci svelte-1j6oeeb" loading="lazy" fetchpriority="auto" decoding="async" draggable="false" onload="this.__e=event" onerror="this.__e=event"/><!----> <!--[-1--><!--]--> <!--[0--><figcaption class="cap svelte-1y2zl5s"> </figcaption><!--]--></figure><!----></div><!----></div><!----> <div class="split svelte-1qwkpf0" style="--split-cols: 1fr 1fr; --split-gap: 2rem; --split-align: start; margin-top: 1.5em; margin-bottom: 1.5em; "><div style="margin-top:-50px;"><figure class="spot svelte-1y2zl5s" style="--blend: exclusion; "><img src="/images/defender-claude/ss1.PNG" alt="" class="ci svelte-1j6oeeb" loading="lazy" fetchpriority="auto" decoding="async" draggable="false" onload="this.__e=event" onerror="this.__e=event"/><!----> <!--[-1--><!--]--> <!--[0--><figcaption class="cap svelte-1y2zl5s"> </figcaption><!--]--></figure><!----></div> <div align="justify">Beyond integrity monitoring, Heimdall provides an investigation layer for MCP environments. Users can inspect syntax-highlighted file diffs, accept or revert changes, quarantine suspicious MCP servers, analyse Claude's runtime logs to reconstruct server activity, and export signed audit reports for later review. The same detection engine is also available through a lightweight headless CLI suitable for continuous monitoring and scripting. <p>
509Heimdall is open source and serves as both a defensive tool and a proof of concept for strengthening the security of local AI agents and MCP-based workflows.</p></div><!----></div><!----> <div class="wide svelte-1wffbbd" style="text-align: left; max-width: none; margin-top: 1.75em; margin-bottom: 1.75em; "><figure class="spot svelte-1y2zl5s" style="--blend: exclusion; "><img src="/images/defender-claude/mm2.PNG" alt="" class="ci svelte-1j6oeeb" loading="lazy" fetchpriority="auto" decoding="async" draggable="false" onload="this.__e=event" onerror="this.__e=event"/><!----> <!--[-1--><!--]--> <!--[0--><figcaption class="cap svelte-1y2zl5s"> </figcaption><!--]--></figure><!----></div><!----> <div class="wide svelte-1wffbbd justify" style="text-align: justify; max-width: none; margin-top: 1.75em; margin-bottom: 1.75em; "><p>Future work focuses on extending Heimdallâs trust model beyond the local machine. Planned improvements include signed software releases with reproducible build metadata, remote anchoring of the cryptographically authenticated audit chain, deeper correlation between runtime behaviour and configuration changes, and replacing the current WebView-based interface with a native desktop frontend to further reduce resource usage. Longer term, the project aims to generalise its integrity model beyond todayâs MCP ecosystem as local AI tooling continues to evolve.</p><!----></div><!----> <div class="wide svelte-1wffbbd justify" style="text-align: justify; max-width: none; margin-top: 1.75em; margin-bottom: 1.75em; "><!----></div><!----> <div class="split svelte-1qwkpf0" style="--split-cols: 1fr 1fr; --split-gap: 2rem; --split-align: start; margin-top: 1.5em; margin-bottom: 1.5em; "><div style="margin-top:-50px;"><figure class="spot svelte-1y2zl5s" style="--blend: exclusion; "><img src="/images/defender-claude/xxxclaude.png" alt="" class="ci svelte-1j6oeeb" loading="lazy" fetchpriority="auto" decoding="async" draggable="false" onload="this.__e=event" onerror="this.__e=event"/><!----> <!--[-1--><!--]--> <!--[0--><figcaption class="cap svelte-1y2zl5s"> </figcaption><!--]--></figure><!----></div> <div align="justify"><table><thead><tr><th>Mode</th><th>Footprint</th></tr></thead><tbody><tr><td>Headless watcher</td><td><code>~7.5</code> MB RAM · ~0.1% CPU · event-driven (no polling)</td></tr><tr><td>Desktop GUI</td><td><code>~35</code> MB engine + WebView2 runtime (mostly shared OS pages)</td></tr></tbody></table> <br/> <p>The headless watcher performs effectively no work while idle, waking only when a monitored MCP configuration or server file changes. Detection is typically sub-second, using an event-driven filesystem watcher rather than periodic polling. The desktop application uses the same detection engine while providing an interface for reviewing changes, intrusion analysis, quarantine, and audit reporting.</p> <p>Or, if you still want to keep the installation paths (since theyâre useful), simply update them and separate them from the performance section:</p> <table><thead><tr><th>OS</th><th>State directory</th></tr></thead><tbody><tr><td>Windows</td><td><code>%LOCALAPPDATA%\MCPMonitor\</code></td></tr><tr><td>macOS</td><td><code>~/Library/Application Support/MCPMonitor/</code></td></tr><tr><td>Linux</td><td><code>~/.local/share/MCPMonitor/</code></td></tr></tbody></table></div><!----></div><!----><!--]--><!--]--></div></div> <!--[-1--><!--]--> <!--[0--><section class="suggest container svelte-gygcht" aria-label="related projects"><p class="rh sgr-h svelte-gygcht">also see</p> <div class="sgr-grid svelte-gygcht"><!--[--><a class="sgr-card svelte-gygcht" href="/projects/pi-handbook/" style="--i:0"><div class="sgr-tile svelte-gygcht"><!--[-1--><div class="tile svelte-4lsiak" aria-hidden="true"><!----><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> 510 <rect class="tile-shape-base" width="100" height="100" /> 511 <g class="shape shape-rest"><path d="M 100 100 Q 60 84 64 100 L 20 100 L 16 84 L 0 100 L 0 0 L 48 68 L 100 0 L 100 52 Z" /></g> 512 <g class="shape shape-hover"><path d="M 100 100 Q 76 100 64 100 L 20 100 L 16 84 L 0 100 L 0 48 L 44 28 L 0 20 L 0 0 L 64 20 L 48 68 L 100 0 Z" /></g> 513 </svg><!----></div><!--]--><!----></div> <span class="sgr-title svelte-gygcht">Prompt injection Handbook + Bot</span></a><a class="sgr-card svelte-gygcht" href="/projects/denmark-hack/" style="--i:1"><div class="sgr-tile svelte-gygcht"><!--[-1--><div class="tile svelte-4lsiak" aria-hidden="true"><!----><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> 514 <rect class="tile-shape-base" width="100" height="100" /> 515 <g class="shape shape-rest"><path d="M 76 80 L 76 100 L 64 80 L 64 100 L 0 100 L 0 0 L 100 0 L 100 16 L 84 28 L 100 40 L 100 100 L 88 80 L 88 100 Z" /></g> 516 <g class="shape shape-hover"><path d="M 76 80 L 76 100 L 64 80 L 64 100 L 0 100 L 0 0 L 100 0 L 100 16 L 32 28 L 100 40 L 100 100 L 88 80 L 88 100 Z" /></g> 517 </svg><!----></div><!--]--><!----></div> <span class="sgr-title svelte-gygcht">How I Hacked 518 Denmark</span></a><a class="sgr-card svelte-gygcht" href="/projects/whfd/" style="--i:2"><div class="sgr-tile svelte-gygcht"><!--[-1--><div class="tile svelte-4lsiak" aria-hidden="true"><!----><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> 519 <rect class="tile-shape-base" width="100" height="100" /> 520 <g class="shape shape-rest"><path d="M 40 100 Q 32 80 20 100 L 0 100 L 0 72 Q 32 60 0 44 L 0 0 L 64 0 L 100 0 L 100 72 L 100 100 Q 68 64 48 100 Z" /></g> 521 <g class="shape shape-hover"><path d="M 40 100 Q 32 80 20 100 L 0 100 L 0 72 Q 32 60 0 44 L 0 0 L 64 0 L 100 0 Q 56 36 92 64 Q 100 72 88 100 Q 68 64 48 100 Z" /></g> 522 </svg><!----></div><!--]--><!----></div> <span class="sgr-title svelte-gygcht">Will Hack 523For Drinks</span></a><a class="sgr-card svelte-gygcht" href="/projects/paktly/" style="--i:3"><div class="sgr-tile svelte-gygcht"><!--[-1--><div class="tile svelte-4lsiak" aria-hidden="true"><!----><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> 524 <rect class="tile-shape-base" width="100" height="100" /> 525 <g class="shape shape-rest">
525<path d="M 64 28 Q 80 36 88 28 L 100 100 L 36 100 L 0 100 L 12 16 Q 100 0 88 36 Q 20 60 44 40 L 36 32 L 44 28 L 64 28 L 80 36 Z" /></g> 526 <g class="shape shape-hover"><path d="M 64 28 Q 84 72 88 28 L 100 100 L 36 100 L 0 100 L 12 16 Q 100 0 88 36 Q 80 12 64 28 Q 40 28 36 32 Q 24 68 48 56 Q 60 48 36 28 Z" /></g> 527 </svg><!----></div><!--]--><!----></div> <span class="sgr-title svelte-gygcht">Paktly</span></a><!--]--></div></section><!--]--> <footer class="container foot svelte-gygcht"><a class="back svelte-gygcht" href="/projects/">â all projects</a></footer></article><!--]--><!----></div><!----></main> <footer class="foot svelte-jz8lnl"><div class="foot-inner svelte-jz8lnl"><div class="foot-brand svelte-jz8lnl"><span class="foot-line svelte-jz8lnl"><a href="/" class="svelte-jz8lnl">xladn0</a> <span class="foot-sep svelte-jz8lnl" aria-hidden="true">·</span> <span>2026</span></span> <span class="foot-line svelte-jz8lnl"><a href="/portfolio/" class="svelte-jz8lnl">portfolio</a></span></div> <ul class="foot-links svelte-jz8lnl"><!--[--><li><a href="https://github.com/2alf" target="_blank" rel="noopener noreferrer" class="svelte-jz8lnl">github</a></li><li><a href="https://xladn0.itch.io/" target="_blank" rel="noopener noreferrer" class="svelte-jz8lnl">itch.io</a></li><li><a href="https://openprocessing.org/@u435103#sketches" target="_blank" rel="noopener noreferrer" class="svelte-jz8lnl">openprocessing</a></li><!--]--> <li><a href="/rss.xml" class="svelte-jz8lnl">rss</a></li></ul></div></footer><!----><!--]--><!--]--> <!--[-1--><!--]--><!--]--> 528 529
529<script> 530 { 531 __sveltekit_lz2u9e = { 532 base: new URL("../..", location).pathname.slice(0, -1) 533 }; 534 535 const element = document.currentScript.parentElement; 536 537 Promise.all([ 538 import("../../app/immutable/entry/start.CyoIDKLS.js"), 539 import("../../app/immutable/entry/app.CLUXOrxk.js") 540 ]).then(([kit, app]) => { 541 kit.start(app, element, { 542 node_ids: [0, 10], 543 data: [null,null], 544 form: null, 545 error: null 546 }); 547 }); 548 } 549 </script>
549 550 </div> 551 </body> 552</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.