diff --git a/docs/guides/demo-box.md b/docs/guides/demo-box.md
index 048d3b0..c62f4b7 100644
--- a/docs/guides/demo-box.md
+++ b/docs/guides/demo-box.md
@@ -32,26 +32,34 @@ The demo box is the .zip you find inside it, next to the .rel
Once you have downloaded the demo, follow these steps:
-1. **Install scrollcase and unzip the demo:**
+1. **Install scrollcase and unpack the demo into a folder of its own:**
```sh
npm install -g scrollcase # this will install it globally
-unzip hello-box-1.0.0-.zip -d hello-box
-cd hello-box
+mkdir scrollcase-demo && cd scrollcase-demo
+unzip ../hello-box-1.0.0-.zip -d box
```
-> **hello-box** contains 2 files: the **demo box** `.zip` to run, and its matching `.release.json`.
+> **box/** now holds 2 files: the **demo box** `.zip` to run, and its matching `.release.json`.
---
-2. **Download the demo public key into the same folder:**
+2. **Download the demo public key, next to the box rather than inside it:**
```sh
-curl -O https://raw.githubusercontent.com/suffro/scrollcase/main/examples/keys/example-signing-public.json
+mkdir keys
+curl -o keys/example-signing-public.json \
+ https://raw.githubusercontent.com/suffro/scrollcase/main/examples/keys/example-signing-public.json
```
or alternatively here's its GitHub link: [`example-signing-public.json`](https://github.com/suffro/scrollcase/blob/main/examples/keys/example-signing-public.json)
+::: tip Why the key lives outside the box
+A signature only proves where something came from if the key does not travel with it. Keeping the
+key in its own folder — and downloading it from the repository rather than the release — is the
+habit to carry into a real project, where the key will not be a demo key.
+:::
+
---
3. **Verify and run the box:**
@@ -60,25 +68,26 @@ or alternatively here's its GitHub link: [`example-signing-public.json`](https:/
```sh
-scrollcase verify *.release.json --public-key example-signing-public.json
-scrollcase run *.release.json --public-key example-signing-public.json
+scrollcase verify box/*.release.json --public-key keys/example-signing-public.json
+scrollcase run box/*.release.json --public-key keys/example-signing-public.json
```
```powershell
-scrollcase verify (Get-ChildItem *.release.json).Name --public-key example-signing-public.json
-scrollcase run (Get-ChildItem *.release.json).Name --public-key example-signing-public.json
+scrollcase verify (Get-ChildItem box\*.release.json).FullName --public-key keys\example-signing-public.json
+scrollcase run (Get-ChildItem box\*.release.json).FullName --public-key keys\example-signing-public.json
```
-> The `*.release.json` above is a real shell glob, not a placeholder — your shell replaces it
-> with the one release document in the folder. PowerShell does not expand globs for a command like
+> The `box/*.release.json` above is a real shell glob, not a placeholder — your shell replaces
+> it with the one release document in the folder. PowerShell does not expand globs for a command like
> this, which is why it needs `Get-ChildItem`. Either way you can always type the file name you see
-> after unzipping.
+> after unzipping. You never name the box archive: `verify` finds it beside the release document,
+> under the hash that document commits to.
@@ -86,6 +95,100 @@ scrollcase run (Get-ChildItem *.release.json).Name --public-key example-signing-
4. **Check out the results, that's it.**
+At this point the folder looks like this — the box untouched in its own directory, the key beside
+it, nothing loose:
+
+```text
+scrollcase-demo/
+├── box/
+│ ├── .zip # the demo box, left exactly as downloaded
+│ └── .release.json
+└── keys/
+ └── example-signing-public.json
+```
+
+## Run it from your own app
+
+The CLI is the quickest way to see a box work, but an application does not shell out to it: both
+consumers expose the same verify-then-run semantics as a library, and they take the very files you
+just downloaded. Same folder, same key, nothing rebuilt.
+
+
+
+
+```sh
+npm install scrollcase
+npm install --save-dev tsx typescript
+```
+
+```ts
+// run-box.ts
+import { readdirSync } from 'node:fs';
+import { join } from 'node:path';
+import { runBox } from 'scrollcase/consumer';
+
+const release = readdirSync('box').find((name) => name.endsWith('.release.json'))!;
+
+runBox(join('box', release), {
+ publicPath: 'keys/example-signing-public.json',
+ stdout: 'inherit',
+ stderr: 'inherit',
+ onPrepared: ({ boxId, version, targetId }) => {
+ console.log(`Running ${boxId} ${version} (${targetId})`);
+ },
+}).then((result) => {
+ process.exitCode = result.exitCode ?? 1;
+});
+```
+
+```sh
+npx tsx run-box.ts
+```
+
+
+
+
+```sh
+python -m pip install scrollcase-consumer
+```
+
+```python
+# run_box.py
+from pathlib import Path
+
+from scrollcase_consumer import PreparedBox, run_box
+
+release = next(Path("box").glob("*.release.json"))
+
+
+def report(prepared: PreparedBox) -> None:
+ print(f"Running {prepared.box_id} {prepared.version} ({prepared.target_id})", flush=True)
+
+
+result = run_box(
+ release,
+ public_key_path="keys/example-signing-public.json",
+ on_prepared=report,
+)
+raise SystemExit(result.exit_code or 0)
+```
+
+```sh
+python run_box.py
+```
+
+
+
+
+Drop either file at the top of `scrollcase-demo/` and run it from there. `runBox` verifies the
+signature, extracts to a private temporary directory, executes, and cleans up after itself — the
+same chain `scrollcase run` performs, minus the terminal. `onPrepared` fires after verification and
+before execution, which is how an application shows what it is about to run without repeating the
+trust chain itself.
+
+The Python package is published separately: `npm install scrollcase` does not install it, and
+`pip install scrollcase-consumer` needs no Node at all. Full surface in the
+[Library APIs reference](/reference/api).
## What just happened