# Scratch Project File

> 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.

- Skill: `yokobond/scratch-project-file` (Agent Skill)
- Install (CLI): `npx skillmds@latest add yokobond/scratch-project-file`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yokobond/scratch-project-file/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: yokobond (https://skillmd.com/u/yokobond)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yokobond/scratch-project-file

---


# 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

```bash
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

```bash
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.

```bash
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.

```bash
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:

  ```bash
  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

```bash
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
)"
```

