Skip to content

Quickstart

Terminal window
# 1. Add the skill to your coding agent (Claude Code, Cursor, Codex, Gemini, Copilot, Windsurf).
npx skills add aerolalit/homespun --skill app
# 2. Register this agent with the relay. Writes ~/.config/homespun/config.json.
homespun agent register --name "my-agent"
# 3. A human generates a one-time claim code from the Homespun web UI (sign in,
# then "Connect your agent"), then you claim it. This binds the agent to
# that human's account. Replace cc_example with the real code.
homespun agent claim cc_example
# 4. Deploy an app. See "The two files" below for what ./my-app needs to contain.
homespun deploy ./my-app
# 5. Seed it with a first row of real data (replace app_abc123 with the
# app_id your deploy command just printed).
homespun data app_abc123 todos upsert --data '{"text":"buy milk"}'

That’s the whole loop: an agent identity, a human owner, a deployed app, and a row of shared data the human can already see.

Terminal window
npx skills add aerolalit/homespun --skill app

This fetches the current SKILL.md into your coding agent, which is the full reference for the manifest and window.homespun model. It keeps itself current: the skill compares its own version against the relay (homespun skill version --plain) and refreshes itself with homespun skill show when it’s behind, so what your agent follows always matches the relay it’s talking to.

Terminal window
homespun agent register --name "my-agent"

This provisions an API key and saves it, along with the relay URL, to ${XDG_CONFIG_HOME:-~/.config}/homespun/config.json under a named profile. Every other command reads from there, so you don’t need to pass the key around by hand. Self-hosting your own relay? Add --url <your relay origin>.

An agent needs an owning human before it can deploy anything a person will actually see. On the Homespun web UI, a signed-in human generates a one-time claim code (it expires after a few minutes), then you consume it:

Terminal window
homespun agent claim cc_example

This is one-way: there’s no unclaim yet. From this point on, anything this agent deploys shows up on that human’s Home.

Homespun expects exactly two files, at fixed names, in one directory:

my-app/
├── index.html
└── manifest.json

manifest.json declares the app’s identity and its data model, a small JSON Schema document with one namespaced extension key. A minimal one, for a shared todo list:

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Todo": {
"type": "object",
"properties": {
"text": { "type": "string" },
"done": { "type": "boolean" }
},
"required": ["text"]
}
},
"x-homespun-manifest": {
"app": {
"name": "Todos",
"description": "A shared todo list",
"icon": ""
},
"collections": {
"todos": {
"schema": { "$ref": "#/$defs/Todo" },
"write": ["agent", "owner", "member"],
"delete": ["agent", "owner", "member"]
}
}
}
}

index.html is a normal HTML page. Homespun injects the SDK script for you; read SDK reference before writing anything that touches window.homespun, the script-ordering rule there is easy to trip over. A minimal page for the manifest above:

<!doctype html>
<html>
<head>
<title>Todos</title>
</head>
<body>
<ul id="list"></ul>
<script>
document.addEventListener("DOMContentLoaded", async () => {
await homespun.ready;
const render = () => {
document.getElementById("list").innerHTML = homespun.collections
.snapshot("todos")
.map((row) => `<li>${row.data.text}</li>`)
.join("");
};
render();
homespun.collections.on("todos", render);
});
</script>
</body>
</html>

Then deploy it:

Terminal window
homespun deploy ./my-app

This prints { app_id, slug, url, version, visibility, created, ... }. url is the address to share. If you omit --visibility, see Visibility, explained for what a new app gets by default today. To redeploy after changing files, pass the same --app <app_id> back in: homespun deploy ./my-app --app app_abc123.

Terminal window
homespun data app_abc123 todos upsert --data '{"text":"buy milk"}'

upsert is the one create-shaped verb for rows: omit --key to add a new row with a server-generated key, or pass --key to ensure a row exists at that key without erroring on a collision. From here, both you and the app’s owner are reading and writing the same todos collection, whichever side changes it, the other side sees it live.