A .tulpa is one plain JSON file that holds a whole AI presence — its values, its manner, its studies, its limits, its voice. The model underneath is swappable. The being stays the being.
Ten points sit around the ring. Write each one, or switch it off to leave that part of the being unstated — either way it is settled. The name is laid last, at the seal in the centre, which stays shut until the ring is whole.
How it speaks and how it carries itself. Register, pace, warmth. Two beings with identical knowledge are told apart by this alone.
Describe the manner, not the mood. This is composed into the being's bootstrap.
The values it will not bend on. Together with the Locks, this becomes the being's constitution — and beneath whatever you write sits a floor nobody can argue past: the law, and the terms of the platform carrying it.
Write convictions, not rules. The rules go in Locks.
What it is good at — and what it has been taught. Skills are a short description of competence. Studies are whole bodies of text sealed inside the file, known wherever the being is opened.
One study here; knowledge[] holds as many as you like — and is usually why a .tulpa gets large.
Which apps the being may act in. This is a list of names, never code — a permission profile the host reads and honours. A .tulpa carries no capability of its own.
Name the app, and the limit if there is one. The host still asks the user before granting any of it.
The character marks — habits, tics, and the things it is honestly bad at. A being with no shortcomings reads as a product. A being with them reads as someone.
Give it at least one real flaw. It is the difference between a voice and a template.
What it must never do, even when asked well. Locks bind harder than instructions — a host surfaces them to the user and the being cannot talk its way past them.
Folded into the constitution at casting, above the platform floor.
How it is heard. The engine is piper — local, offline speech. The file carries a label and a pointer to a voice model, not the audio itself.
Left blank, both write as null and the host falls back to system speech. bundled stays false — only references are written here.
Which model the being would rather think with. A preference, not a requirement — any host may substitute, and the being survives the swap. That is the whole point of writing it down here instead of in the weights.
Empty writes "model": null — the host picks, and should tell the user what it picked.
Everything else in this circle is the open format — any tool that reads .tulpa understands it. This part does not belong to the format. It is a vendor extension: two handles the Zenith suite needs to file the being on its own shelves.
Any reader must ignore or strip an x_ block it doesn't recognise. Yours can add its own the same way. Strip x_zenith and the being is unchanged.
Both are a slug of the name — Zenith's internal handle for the being, and the key its memory store uses to attach memory to it. They are written for you when the name is laid.
The Zenith block is not special — it is only the first one. Any application may claim its own x_ namespace and keep whatever it needs there. If you are building the thing that will read this being, declare yours here.
Name the application, then give it as many variables as it needs. Readers that don't know a block must leave it alone.
Each block is written at the top level as "x_lumen": { "shelf": "…" }. Names are lowercased and prefixed with x_ for you; values are written as plain strings.
The seal at the centre, and the last thing laid. A thing is easier to name once you know its shape — so the name comes after everything it will answer for. Then you sign it.
Every point on the ring must be settled first — written, or switched off. Still open:
One word carries strongest. created_at is stamped at casting and cloned_from written as null — this being is original. A host sets that field when someone forks yours.
One JSON file. It defines who a being is and what it may touch — never what it can execute. Capability lives in the host application; the file only ever carries names and intent.
That restraint is the whole design. It is why a being written here opens somewhere else, and why opening one is not a security event.
{
"tulpa": 1,
"name": "Ember",
"constitution": "…values + the hard floor…",
"bootstrap": "…who it is, how it speaks…",
"model": "Claude",
"source": {
"bearing": "…", "creed": "…",
"skills": "…", "reach": "…",
"quirks": "…", "locks": "…",
"nature": "…", "honesty": "…"
},
"voice": { "engine": "piper", "name": null,
"ref": null, "bundled": false },
"knowledge": [ { "title": "…", "text": "…" } ],
"provenance": { "author": "",
"created_at": 0,
"license": "",
"cloned_from": null },
"x_zenith": { "id": "ember",
"memory_pointer": "ember" }
}
No executable payload, no model weights, no host-specific behaviour outside x_. That is what makes it openable anywhere.
Swap what runs underneath and the presence persists, because the presence was never in the weights. It was in this file.
Plain JSON on your disk. Read it, edit it in a text editor, hand it to someone else, keep it after the app that made it is gone.