PageSourceSearch

https://xladn0.com/projects/claude-defender/

html xladn0.com collected 2026-10-05 03:08:55 UTC 42,506 bytes, 552 lines download raw bytes

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 &amp; 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.