# Files, clipboard & usage

> Reading, writing, listing and watching files in the workspace folder; copying to the clipboard; the workspace's token usage and costs.

Source: https://docs.neurosquad.ai/en/card-sdk/api/files

## Files in the workspace folder

Needs [`fs.read`](https://docs.neurosquad.ai/en/card-sdk/permissions#fs-read) to read and [`fs.write`](https://docs.neurosquad.ai/en/card-sdk/permissions#fs-write)
to change. Every path is **relative to the workspace folder** — the project folder the user chose
for the workspace — with `/` or `\` separators; `''` or `'.'` is the folder itself.

```ts
// Read
const pkg = JSON.parse(await card.fs.readText('package.json')) as { name: string }
const logo = await card.fs.readBytes('public/logo.png')
const info = await card.fs.stat('src/index.ts')          // { path, type, size, mtimeMs }
const { entries, truncated } = await card.fs.list('src', { recursive: true, maxEntries: 2000 })
render(pkg.name, logo.byteLength, info.size, entries.length, truncated)

// Write (atomically: a temporary file, then a rename)
await card.fs.writeText('reports/latest.md', '# Report\n', { createDirs: true })
await card.fs.mkdir('reports/archive')
await card.fs.trash('reports/old.md')                     // to the OS trash; there is no hard delete

// Write only if nobody changed it since you read it
const before = await card.fs.stat('TODO.md')
await card.fs.writeText('TODO.md', '- [ ] ship it\n', { ifMtimeMs: before.mtimeMs })
```

| Method | Result |
| --- | --- |
| `stat(path)` | `{ path, type: 'file' or 'directory', size, mtimeMs }` |
| `list(path?, { recursive?, maxEntries? })` | `{ entries: { path, type, size? }[], truncated }` — ≤ 5 000 entries; recursive listing skips `.git` and `node_modules` unless you list inside them |
| `readText(path, { maxBytes? })` | UTF-8 text |
| `readBytes(path, { maxBytes? })` | `Uint8Array` |
| `read(path, { encoding?, maxBytes? })` | `{ data, encoding, size, truncated }` — the raw result |
| `writeText(path, text, { createDirs?, ifMtimeMs? })` | the new `FsStat` |
| `writeBytes(path, bytes, { createDirs?, ifMtimeMs? })` | the new `FsStat` |
| `mkdir(path)` | `FsStat` |
| `trash(path)` | moves a file or folder to the OS trash (60 a minute) |
| `watch(path, handler, { recursive? })` | resolves with a function that stops watching |

Reads and writes are up to 10 MB each.

**Watching:**

```ts
const stop = await card.fs.watch('reports', ({ path, type }) => {
  render(`${path} ${type === 'rename' ? 'was added or removed' : 'changed'}`)
}, { recursive: true })
// later
await stop()
```

Changes are debounced by 100 ms; at most 20 watchers per card; watchers stop when the card's page
is unloaded (watch again after `launch === 'resumed'`).

**The fence.** Absolute paths and any `..` are refused outright. After that, the app resolves the
real path of the target (or, for a new file, of its nearest existing folder) and refuses it unless
it is inside the workspace folder — so a symbolic link or junction that points outside does not
help. All of these fail with `FS_DENIED`; other file system problems (not found, not a folder…)
with `FS_ERROR`, and a failed `ifMtimeMs` check with `FS_ERROR` and `data.conflict`.

**No file access at all** — every `fs.*` call fails with `FS_DENIED` — when the workspace folder is
the user's home folder, the root of a drive, or contains NeuroSquad's own data folder.

### Protected paths

Writes, `mkdir` and `trash` are refused (`FS_DENIED`) for these, at any depth, checked on the path
you give and on the real path behind any link — they would let a card run code through git, the
user's agents or their tools:

- anything inside a folder named `.git` (including a submodule's or worktree's `.git` file),
`.gitmodules`, `.gitattributes`;
- the folders `.claude`, `.codex`, `.cursor`, `.gemini`, `.qwen`, `.opencode`, `.kilocode`,
`.windsurf`, `.continue`, `.vscode`, `.idea`, `.husky`, `.devcontainer` and `.github/workflows`;
- the files `.mcp.json`, `CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md`, `GEMINI.md`, `QWEN.md`,
`.cursorrules`, `.windsurfrules`, `opencode.json`, `opencode.jsonc`, `.envrc`, `.npmrc`, `.yarnrc`,
`.yarnrc.yml`, `.pnpmfile.cjs`.

Reading them is allowed with `fs.read`.

> There is no file picker in version 1: a card works with the workspace folder only. To point a card
> at a file, let the user type the path in a setting, or receive an `ns:file-ref` over a port.

## Clipboard

Needs [`clipboard.write`](https://docs.neurosquad.ai/en/card-sdk/permissions#clipboard-write) — a good candidate for an
optional permission.

```ts
await card.copyText('npm test -- --grep checkout')
```

At most once a second, ≤ 1 MB. The browser's own `navigator.clipboard` does not work in a card, and
reading the clipboard is not possible at all.

## Token usage and costs

Needs [`usage.read`](https://docs.neurosquad.ai/en/card-sdk/permissions#usage-read).

```ts
const summary = await card.usage('7d') // 'today' (default), '7d' or '30d'
const dollars = (summary.totalCostMicroUsd / 1_000_000).toFixed(2)
render(`$${dollars}${summary.partial ? ' + unpriced models' : ''}`)
for (const row of summary.rows) {
  render(row.name, row.harness, row.inputTokens, row.outputTokens, row.costMicroUsd ?? 'no price')
}
```

The same numbers as the app's [Usage](https://docs.neurosquad.ai/en/usage) page, for the card's workspace: `period`, `from` and
`to` (local days, end exclusive), and per agent `inputTokens`, `outputTokens`, `cacheReadTokens`,
`cacheWriteTokens` and `costMicroUsd`. Money is in **whole micro-dollars** (1 000 000 = $1). A model
without a known price has `costMicroUsd: null` — never `0` — is left out of `totalCostMicroUsd`, and
sets `partial: true`.
