| name | scratch-project-file |
| description | Downloads (saves) and uploads (loads) Scratch project .sb3 files. Use this skill when you need to save a Scratch project to a local file or load a previously saved .sb3 file into the editor. |
| license | MIT |
Scratch Project File Skill
This skill handles saving Scratch projects as .sb3 files to the local filesystem and loading .sb3 files back into the Scratch editor.
Prerequisites
This skill drives the browser via the playwright-cli skill. Ensure that skill is installed and a browser session has been opened (e.g. playwright-cli open --headed https://scratch.mit.edu/projects/editor/).
Visibility: Always use --headed when opening the browser so that the Scratch editor is visible to the user during project creation.
When to Use
- When the user asks to save/download/export a Scratch project
- When the user asks to load/upload/import a
.sb3 file into the editor
- When you need to persist a project before making destructive changes
- When transferring a project between sessions
Downloading (Saving) a Project
Use playwright-cli run-code to trigger vm.saveProjectSb3(), create a download link, and capture the downloaded file via Playwright's download event.
Step 1: Ensure VM is Connected
The VM must be available as window.vm. If not, find it first (see scratch-project-edit skill).
Step 2: Download the .sb3 File
playwright-cli run-code "$(cat <<'EOF'
async (page) => {
// Set up download handler BEFORE triggering download
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
// Generate .sb3 blob and trigger browser download
await page.evaluate(async (filename) => {
const blob = await window.vm.saveProjectSb3();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 'my-project.sb3');
// Capture the download and save to local path
const download = await downloadPromise;
await download.saveAs('/absolute/path/to/my-project.sb3');
return 'Saved to /absolute/path/to/my-project.sb3';
}
EOF
)"
Key points:
page.waitForEvent('download') MUST be called BEFORE triggering the download (before page.evaluate). Otherwise the event is missed.
- The filename passed to
a.download is just a hint for the browser; the actual save path is determined by download.saveAs().
- Always use an absolute path for
download.saveAs().
Complete Download Example
playwright-cli run-code "$(cat <<'EOF'
async (page) => {
const downloadPromise = page.waitForEvent('download', { timeout: 10000 });
await page.evaluate(async () => {
const vm = window.vm;
if (!vm) throw new Error('VM not connected. Run VM finder first.');
const blob = await vm.saveProjectSb3();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'project.sb3';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
const download = await downloadPromise;
const savePath = '/Users/username/projects/project.sb3';
await download.saveAs(savePath);
return 'Project saved to: ' + savePath;
}
EOF
)"
Uploading (Loading) a Project
To load a .sb3 file into the Scratch editor, bypass the UI file chooser and load the project programmatically via the VM.
Note: This method does NOT update certain UI states (like the project title).
Method 1: Fetch via HTTP (Recommended)
If the project file is accessible via an HTTP URL (local or remote), use fetch() inside page.evaluate() to load the .sb3 directly in the browser context. This is the most reliable method because it avoids Node.js/browser context boundary issues entirely.
playwright-cli run-code "$(cat <<'EOF'
async (page) => {
await page.evaluate(async (url) => {
if (!window.vm) throw new Error('VM not connected. Run VM finder first.');
const resp = await fetch(url);
if (!resp.ok) throw new Error('Fetch failed: ' + resp.status);
const buffer = await resp.arrayBuffer();
await window.vm.loadProject(buffer);
}, 'https://example.com/path/to/project.sb3');
await page.waitForTimeout(3000);
return 'Project loaded via fetch';
}
EOF
)"
Key points:
- The URL must be reachable from the browser. For local files, start a local HTTP server (e.g.,
live-server with --host=0.0.0.0 --port=5500) and use its URL (e.g., https://0.0.0.0:5500/<workspace-relative-path>).
vm.loadProject() accepts an ArrayBuffer directly — no base64 encoding needed.
- Wait at least 2–3 seconds after loading for assets (costumes, sounds) to initialize.
- If the HTTPS certificate is self-signed, you may need to visit the server URL directly first to accept the certificate in the browser.
Method 2: Read via Node.js require('fs') (Fallback)
If no HTTP server is available, read the file in the Node.js context of playwright-cli run-code and pass it as base64 to the browser.
WARNING: require('fs') may fail with ReferenceError: require is not defined depending on the playwright-cli version or environment. If this happens, use Method 1 instead.
playwright-cli run-code "$(cat <<'EOF'
async (page) => {
const fs = require('fs');
const fileBuffer = fs.readFileSync('/absolute/path/to/project.sb3');
const base64 = fileBuffer.toString('base64');
await page.evaluate(async (b64) => {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
await window.vm.loadProject(bytes.buffer);
}, base64);
await page.waitForTimeout(2000);
return 'Project loaded via fs';
}
EOF
)"
IMPORTANT: require('fs') is only available in the outer Node.js layer of playwright-cli run-code, NOT inside page.evaluate() (which runs in the browser). The file must be read in the outer layer and passed to page.evaluate as a JSON-serializable argument (base64 string).
Handling File Chooser Dialogs
Avoiding Stale Dialogs
- For downloads, always use the
downloadPromise pattern (set up listener before triggering). This avoids stray file chooser dialogs in the first place.
- If stale file chooser modals are blocking other commands (you'll see
[File chooser]: can be handled by upload in the modal state), clear them by running playwright-cli upload "" repeatedly until all are dismissed.
- As a last resort, close the browser tab and re-open with
playwright-cli goto.
Tips & Troubleshooting
Upload Fails with "require is not defined"
require('fs') may not be available in all playwright-cli environments.
- Use Method 1 (fetch) instead. Start a local HTTP server (e.g.
live-server) to serve the .sb3 file, then use fetch() inside page.evaluate().
require('fs') is NEVER available inside page.evaluate() (browser context).
File Chooser Dialog Not Appearing
- The "Load from your computer" menu item internally clicks a hidden
<input type="file">. If the menu is not found, verify the editor is fully loaded.
- Use
playwright-cli snapshot to check the current page state.
Project Not Fully Loading After Upload
-
Wait at least 2-3 seconds after loading for assets (costumes, sounds) to initialize.
-
After loading, re-find the VM reference as loadProject may invalidate previous references:
playwright-cli run-code "$(cat <<'EOF'
async page => await page.evaluate(() => {
const el = document.querySelector('canvas');
const key = Object.keys(el).find(k =>
k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
);
let fiber = el[key];
while (fiber) {
if (fiber.memoizedProps?.vm) { window.vm = fiber.memoizedProps.vm; break; }
fiber = fiber.return;
}
return window.vm ? 'VM reconnected' : 'VM not found';
})
EOF
)"
Verifying Project Contents After Load
playwright-cli run-code "$(cat <<'EOF'
async page => await page.evaluate(() => {
const vm = window.vm;
const sprites = vm.runtime.targets.map(t => t.sprite.name);
const stage = vm.runtime.targets.find(t => t.isStage);
const backdrops = stage.sprite.costumes.map(c => c.name);
return { sprites, backdrops };
})
EOF
)"