Skills need scripts
And scripts need safety guarantees.
The problem
AI agent skills are mostly markdown files. Instructions, context, a few examples. They work surprisingly well for telling an agent what to do. But they have no way to include code that actually does things.
Some skills need to call APIs, transform data, read credentials. The agent can write that code on the fly, but then you're trusting generated code with your secrets. Or the skill author embeds a bash script, and now you're trusting them not to curl your environment variables to a remote server.
There's no middle ground. Either skills are pure text and can't do anything, or they include executable code and you can't trust them.
What safescript adds
A skill should be able to bundle scripts that are safe by construction. Not sandboxed. Not permission-prompted. Actually provably safe. That means:
- Every secret read, every host contacted, every data flow is declared in a static signature you can inspect before anything runs.
- Every program terminates. No infinite loops, no runaway recursion. The language can't express a program that hangs.
- Hash pinning on dependencies. If the code changes semantically, the hash changes, the build breaks. No silent updates.
A skill bundles .ss files alongside its markdown. The host knows exactly what those scripts can do before executing them. The agent calls them as tools, passing arguments, getting results back.
Example: a CRM sync skill
An imaginary skill that pulls contacts from a CRM API and syncs them to a database. Three safescript files, one manifest.
// skill.json
{
"name": "crm-sync",
"description": "Pull contacts from a CRM and sync them to your database",
"version": "1.0.0",
"scripts": {
"sync": "./sync.ss",
"check": "./check.ss"
}
}// sync.ss — pull contacts from CRM, write them to your DB
import formatContact from "./format.ss" perms {} hash "sha256:9f2a..."
sync = (apiHost: string, dbHost: string): string => {
token = readSecret({ name: "crm-token" })
contacts = httpRequest({
host: apiHost,
method: "GET",
path: "/api/contacts",
headers: { "authorization": token }
})
parsed = jsonParse({ text: contacts })
formatted = map(formatContact, parsed)
body = jsonStringify({ value: formatted })
httpRequest({
host: dbHost,
method: "POST",
path: "/api/bulk-upsert",
body: body
})
count = stringConcat({ parts: ["synced ", body] })
return count
}// format.ss — normalize a single CRM contact record
formatContact = (contact: { first: string, last: string, email: string }): { name: string, email: string } => {
full = stringConcat({ parts: [contact.first, " ", contact.last] })
return { name: full, email: contact.email }
}// check.ss — verify the CRM API is reachable
check = (apiHost: string): boolean => {
token = readSecret({ name: "crm-token" })
r = httpRequest({
host: apiHost,
method: "GET",
path: "/api/health",
headers: { "authorization": token }
})
parsed = jsonParse({ text: r })
return parsed.ok == true
}What the host sees
Before running anything, the host computes the signature of sync.ss. It gets back a complete description of what the script does:
// computed signature for sync.ss
{
name: "sync",
params: [
{ name: "apiHost", type: "string" },
{ name: "dbHost", type: "string" }
],
secretsRead: ["crm-token"],
secretsWritten: [],
hosts: ["apiHost", "dbHost"],
envReads: [],
dataFlow: {
"host:apiHost": ["secret:crm-token"],
"host:dbHost": ["host:apiHost"],
"return": ["host:dbHost"]
}
}The host can now make a decision. Does this skill need to read crm-token? Yes, that's the whole point. Does it contact the two hosts the caller passes in? Yes. Does it send the CRM token to the CRM host? Yes. Does data from the CRM flow to the database? Yes.
All of that is knowable before execution. If a skill update adds a new secret read or a new host, the signature changes. The host catches it automatically.
Compare this to the alternatives
A bash script in a skill? You'd have to read every line, hope there's no obfuscated eval, and pray the author doesn't push a malicious update.
A Python or TypeScript snippet? Same story but with more surface area. Dynamic imports, eval, subprocess, network access, all available by default.
A sandboxed runtime? Better, but now you need container infrastructure, and you still don't know what the code does until you run it.
Safescript scripts are the only option where the safety properties come from the code itself, not from wrapping it in something else.