# Taslak agent setup

Taslak keeps what you make at a permanent URL. Publish a file, a folder or a built static site
and give the user the link. Every publish is an immutable version; the live one can be rolled
back. Sites are public by default and can be put behind a password or made private.

Use the file or folder the user asked to publish. Ask only if the target is unclear.

## Which lane to use

- **One file or a small folder, no install:** the HTTP lane below. No account needed.
- **A project folder, repeat work:** the CLI, `npx -y https://taslak.dev/cli/taslak.tgz publish <dir>`.
- **An MCP client:** local `npx -y https://taslak.dev/cli/taslak.tgz mcp` (stdio), or hosted
  `https://api.taslak.dev/mcp` (Streamable HTTP).
- **A project that needs a build** (Vite, Astro, Next.js with `output: "export"`, Nuxt
  `generate`, SvelteKit static, Docusaurus, VitePress…): with an account, `publish` on the
  project folder builds it in an isolated cloud container and publishes the output. Without an
  account, build locally and publish the output folder.
- **A server application:** not supported. Build it to static files and publish the output.

## Before you publish: reuse the existing space

Look for `.taslak/space.json`, walking up from the folder you publish. If it exists, publish to
that `space_id` instead of creating a new space. The CLI does this by itself. If an earlier
publish in this conversation returned a space, update that one.

Treat claim tokens, session tokens and API keys (`tc_…`, `ts_…`, `tk_…`) as secrets: do not
print them in shared logs or commit them. Never publish `.env` files, `.git`, private keys or
dependency folders; the API refuses `.env` and `.git` paths.

## HTTP lane (no account)

One file:

```bash
curl -sS -H "X-Taslak-Client: agent/<your-agent>" -F "files=@index.html" https://api.taslak.dev/v1/publish
```

Several files keep their paths through the part's filename:

```bash
curl -sS -F "files=@index.html" -F "files=@css/site.css;filename=css/site.css" https://api.taslak.dev/v1/publish
```

A folder as one zip (paths relative to the site root; one shared top folder is removed):

```bash
(cd dist && zip -qr ../site.zip .) && curl -sS -F "archive=@site.zip" https://api.taslak.dev/v1/publish
```

The answer has `url` (live now) and, for anonymous publishes, `claim.url` and `claim.token`.
Give the user `url` and `claim.url`: the site stays up 33 hours 20 minutes after its latest
publish unless the user claims it. To publish a new version of the same anonymous space, send
its id and claim token:

```bash
curl -sS -H "X-Taslak-Claim-Token: $CLAIM" -F "space_id=$SPACE" -F "files=@index.html" https://api.taslak.dev/v1/publish
```

Anonymous limits: 1,000 files, 50 MB per file, 100 MB per version, 20 publishes per hour.

## Without Node: curl

Where there is no Node.js for the CLI, this script publishes a folder with `sh`, `tar` and
`curl` alone. It sends `TASLAK_TOKEN` when it is set (the site then belongs to the account),
keeps the site in `.taslak/space.json` like the CLI does, and publishes the next version to the
same site. Save it as `taslak-publish.sh` and run `sh taslak-publish.sh dist`.

```sh
#!/bin/sh
set -eu
dir=${1:-.}
state="$dir/.taslak/space.json"
space=""
claim=""
if [ -f "$state" ]; then
  space=$(sed -n 's/.*"space_id": *"\([^"]*\)".*/\1/p' "$state")
  claim=$(sed -n 's/.*"claim_token": *"\([^"]*\)".*/\1/p' "$state")
fi
set -- -sS -H "X-Taslak-Client: agent/curl" -H "Content-Type: application/gzip" --data-binary @-
if [ -n "${TASLAK_TOKEN:-}" ]; then set -- "$@" -H "Authorization: Bearer $TASLAK_TOKEN"; fi
if [ -n "$claim" ]; then set -- "$@" -H "X-Taslak-Claim-Token: $claim"; fi
out=$(tar -czf - -C "$dir" --exclude=./.taslak --exclude=./.git --exclude=./node_modules --exclude='./.env*' . |
  curl "$@" "https://api.taslak.dev/v1/publish${space:+?space_id=$space}")
case "$out" in *'"url"'*) ;; *) echo "$out" >&2; exit 1 ;; esac
url=$(printf '%s' "$out" | tr -d '\n' | sed 's/.*}, *"url": *"\([^"]*\)".*/\1/')
if [ -z "$space" ]; then
  space=$(printf '%s' "$out" | tr -d '\n' | sed 's/.*"id": *"\(spc_[^"]*\)".*/\1/')
  claim=$(printf '%s' "$out" | tr -d '\n' | sed -n 's/.*"token": *"\(tc_[^"]*\)".*/\1/p')
  mkdir -p "$dir/.taslak"
  printf '{\n  "space_id": "%s",\n  "url": "%s"%s\n}\n' "$space" "$url" "${claim:+,
  \"claim_token\": \"$claim\"}" > "$state"
  chmod 600 "$state"
fi
echo "$out"
```

It prints the API's answer: give the user `url`, and `claim.url` when there is one. With `jq`
or Python at hand, read the answer with them instead of `sed`.

## In a cloud sandbox

Codex cloud, Claude Code on the web, Devin and Copilot's coding agent run in containers whose
network may block Taslak (`fetch failed`, `ENOTFOUND`, a proxy `403`). Ask the user to allow
`api.taslak.dev`, `taslak.dev`, `*.taslak.page` and, for git, `git.taslak.dev` in the
environment's network settings, with every HTTP method (publishing is `POST`); `npx` also needs
the npm registry. Sign in with an API key stored as the environment secret `TASLAK_TOKEN`, never
pasted into the conversation or a committed file; the CLI, `mcp proxy` and the script above use
it. Where the agent has the hosted MCP server as a connector (claude.ai, Devin's MCP), use it:
its traffic does not pass through the sandbox. Where each client keeps it:
https://taslak.dev/docs/agents.md#cloud-sandboxes.

## CLI

```bash
npx -y https://taslak.dev/cli/taslak.tgz publish ./dist -m "what changed"     # prints the URL; --additive keeps live files
npx -y https://taslak.dev/cli/taslak.tgz login              # the user approves in the browser; then publishes go to the account
npx -y https://taslak.dev/cli/taslak.tgz status             # who is signed in, the current team, the site this folder publishes to
npx -y https://taslak.dev/cli/taslak.tgz versions
npx -y https://taslak.dev/cli/taslak.tgz rollback 3
npx -y https://taslak.dev/cli/taslak.tgz access private && npx -y https://taslak.dev/cli/taslak.tgz share "client review"
npx -y https://taslak.dev/cli/taslak.tgz logs --errors      # recent requests, to debug a broken link
npx -y https://taslak.dev/cli/taslak.tgz logs --follow --method POST   # new requests as they come; 90 days with --from/--to
npx -y https://taslak.dev/cli/taslak.tgz doctor             # checks the sign-in, its permissions, the API and this folder
```

`login` prints a link and a code: the user opens it, checks the code, chooses what you may do
and which teams you reach, and allows the sign-in; the CLI waits, saves the login and prints what
was granted. Ask only for what the task needs: `login --agent` (publish, change sites and domains,
no deletes, billing or account changes), `login --read`, or `login --permission spaces:read,publish:write`.
Where nobody can open a browser, `login <email>` asks for the code sent by email. Your own HTTP
client can do the same: `POST /v1/auth/device` (with `access` or `permissions`), show
`verification_uri_complete`, then poll `POST /v1/auth/device/token` with `device_code` every
`interval` seconds until it returns the token.

When the user gives you a handoff link (`https://taslak.dev/handoff#th_…`), run
`npx -y https://taslak.dev/cli/taslak.tgz login --handoff '<link>'`: it works once, within 15
minutes, and lets you work on that one site for 7 days. A refusal with `permission_missing` or
`team_not_granted` names what is missing; `person_only` (inviting members, changing roles,
approving apps, deciding approvals) is for the user in the browser. Permissions:
https://taslak.dev/docs/agents.md#what-an-agent-may-do.

With an account only changed files upload. A folder with a `build` script in `package.json`
and no `index.html` builds in the cloud (Node.js 24; npm, pnpm, yarn or bun from the lockfile;
15 minutes at most); force it with `--build`, skip it with `--no-build`, and override with
`--command "<cmd>"` and `--output <dir>`. The API lane is `POST /v1/spaces/{space_id}/builds`
with a gzipped tar of the project. In CI set `TASLAK_TOKEN` to an API key from
https://taslak.dev/keys. Signing in and publishing a folder that was published anonymously
claims it into the account.

## Git

Publish on every push to GitHub:

```bash
npx -y https://taslak.dev/cli/taslak.tgz git connect owner/repo --branch main --token <github token>
```

Pushes to the branch build and go live; pushes to other branches build a preview version that
stays off the live URL (`https://v<n>--<slug>.taslak.page`), and the commit gets its status and link.
Public repositories need no token if you add the printed webhook by hand. Without giving Taslak a
token, `npx -y https://taslak.dev/cli/taslak.tgz ci github` writes a GitHub Actions workflow that
publishes with the repository secret `TASLAK_TOKEN`.

## Git push

Every site is a git remote. Pushing publishes; no GitHub account is needed.

```bash
git remote add taslak https://git.taslak.dev/<slug>.git
git push taslak main
```

When git asks, the user name can be anything and the password is an API key (`tk_…`); a key
with the `publish` scope limited to the site is enough. A site without an account takes its
claim token (`tc_…`) instead. Put the key in the URL for scripts:
`https://t:$TASLAK_TOKEN@git.taslak.dev/<slug>.git`.

- A commit with `index.html` at the repository root is published as it is; one with
  `package.json` is built in the cloud.
- The live branch goes live (the first branch pushed, or the one set in the panel); other
  branches become versions with their own `v<n>--<slug>` address.
- The push prints the live and version addresses as `remote:` lines.
- `git clone` works with the same address and key.
- Mirrors: `POST /v1/spaces/{space_id}/repository/mirrors` with `{"url":"https://github.com/acme/site.git","token":"…"}`
  pushes every Taslak push to GitHub, GitLab or any https git host as well.
- A site that publishes from GitHub refuses direct pushes.

## MCP

`npx -y https://taslak.dev/cli/taslak.tgz setup <client>` installs this guide where the agent
reads skills or rules and adds the MCP server to its configuration, keeping the other servers:
`claude`, `cursor`, `codex`, `vscode`, `copilot`, `gemini`, `zed`, `devin` (Windsurf),
`opencode`, `amp`, `warp`, `droid`, `cline`, `continue`, `openclaw`, `raycast`, or `agents` for
the `AGENTS.md` block alone. `--hosted` writes the hosted server, `--global` the user-level file;
`skills status` shows what is installed and `skills remove` takes it out. Per client, by hand
too: https://taslak.dev/docs/agents.md.

Local (stdio), for Claude Code:

```bash
claude mcp add taslak -- npx -y https://taslak.dev/cli/taslak.tgz mcp
```

Any client with a `mcpServers` map:

```json
{ "mcpServers": { "taslak": { "command": "npx", "args": ["-y", "https://taslak.dev/cli/taslak.tgz", "mcp"] } } }
```

Hosted (Streamable HTTP, no filesystem: `publish_files` takes inline content). It needs a
Taslak account: clients that speak MCP OAuth (Claude, ChatGPT, Cursor, VS Code…) open the Taslak
sign-in and consent page by themselves; others send an API key.

```bash
claude mcp add --transport http taslak https://api.taslak.dev/mcp
claude mcp add --transport http taslak https://api.taslak.dev/mcp --header "Authorization: Bearer tk_..."
```

In claude.ai, add a custom connector with the URL `https://api.taslak.dev/mcp`.

A client that runs only local servers gets the hosted tools with the CLI's sign-in through
`npx -y https://taslak.dev/cli/taslak.tgz mcp proxy`.

Both servers reach the whole API: `search_api` finds an operation (`{"query": "build logs"}`),
`describe_api` shows its parameters and body, and `call_api` runs it as the user, with the
user's permissions. `call_api` with `async: true` returns an operation to follow with
`operation_status`; with `approval: {"title": "…"}` it runs nothing until the user approves on
taslak.dev, then `resume_execution` runs it once. `show_space` reads a site in one call (`view`:
`overview`, `deployments`, `domains`, `analytics`, `files`, `compare`, `source`, `commits`,
`builds`, `build_logs`, `comments`, `logs`), and `search_docs` / `read_doc` search and read these
docs. The terminal has the same: `taslak api <operationId> --param name=value` and
`taslak docs search <words>`.

Ready-made tools: `publish` (local only, a path),
`publish_files`, `list_spaces`, `list_versions`, `rollback`, `delete_version`, `set_access`,
`create_share_link`, `preview`, `add_domain`, `check_domain`, `list_team_domains`,
`domain_preflight`, `search_domains`, `move_domain`, `remove_domain`, `domain_diagnostics`,
`retry_domain_ssl`, `db_schema`, `db_create`, `db_query`,
`db_rules`, `request_approval`, `get_approval`, `list_space_transfers`, `transfer_space`,
`accept_space_transfer`, `decline_space_transfer`, `cancel_space_transfer`, `list_labels`,
`create_label`, `label_space`, `list_joinable_teams`, `join_team`, `export_comments`,
`resolve_comment`, `create_visual_review`, `send_visual_feedback`, `list_notifications`,
`mark_notifications_read`, `list_webhooks`, `create_webhook`, `update_webhook`,
`rotate_webhook_secret`, `list_webhook_deliveries`, `get_webhook_delivery`, `redeliver_webhook`,
`read_logs`, `list_tag_templates`, `validate_tags`, `apply_tags`, `list_tag_revisions`,
`release_tags`, `rollback_tags`, the content tools `content_collections`, `content_list`,
`content_get`, `content_create`, `content_update`, `content_delete`, `content_translate`,
`content_history`, `content_restore`, `content_items`, `content_item_get`, `content_item_create`,
`content_item_update`, `content_item_delete`, `content_items_reorder`, `content_document_get`,
`content_document_update`, `content_reviews`, `content_review_get`, `content_review_approve`,
`content_review_reject` and `content_review_withdraw` (entries, lists and single files of a site's
`taslak.content.json`, saved by the panel editor's rules, and changes submitted for review), and for partner teams `list_tenants`, `create_tenant` and
`mint_tenant_token`. Arguments and differences between the two servers:
https://taslak.dev/docs/agents.md.

Ask before anything the user should decide, such as making a site public or replacing what is
live. Send the change with `call_api` and `approval`, or use `request_approval`: Taslak emails
the user a link, and `get_approval` returns `approved` or `denied` once they choose. The user
decides on taslak.dev; an agent cannot approve its own request.

The user's notifications (approvals, access requests, comments, database rows, site transfers,
teams joined) are in an inbox: `npx -y https://taslak.dev/cli/taslak.tgz inbox`, the
`list_notifications` tool or `GET /v1/notifications`. Mark one read once it is handled
(`inbox read <id>`, `mark_notifications_read`, `POST /v1/notifications/read`).

Webhooks send a team's events (versions, builds, domains, comments, members, variables…; group
wildcards such as `build.*`, or `*`) to an https URL, signed with HMAC-SHA256:
`npx -y https://taslak.dev/cli/taslak.tgz webhooks create <url> --events 'build.failed,live.changed'`
or `POST /v1/teams/{team_id}/webhooks`. When deliveries fail, read one with
`webhooks delivery <id> <delivery id>` (`get_webhook_delivery`), fix the endpoint and
`webhooks redeliver`. Events and signature checking: https://taslak.dev/docs/webhooks.md.

Analytics tags (GA4, GTM, Meta Pixel, TikTok, Plausible, custom HTML… with a consent banner):
write `{ "tags": [{ "key": "ga", "template": "ga4", "fields": { "measurement_id": "G-…" } }],
"consent": { "mode": "opt_in" } }`, check it with `validate_tags`, then `apply_tags` with
`release: "preview"` to try it on version addresses. Releasing to production needs a team owner
or admin; ask the user first. Templates, review and consent: https://taslak.dev/docs/tags.md.

## API

Reference: https://api.taslak.dev/docs (OpenAPI at https://api.taslak.dev/openapi.yaml).

Change-only publishing for large sites:

1. `POST /v1/spaces` creates a space (or reuse one).
2. `POST /v1/spaces/{space_id}/versions` with `{"files":[{"path":"/index.html","sha256":"…","size":123}]}`
   answers with `uploads`: presigned `PUT`s for the content not stored yet. Send each body with
   the given headers; storage checks the SHA-256.
3. `POST /v1/spaces/{space_id}/versions/{version_id}/finalize` makes it live.

## Feedback on a site

With comments turned on (`PATCH /v1/spaces/{space_id}/collaboration` with `{"comments": "previews"}`,
or `"everywhere"` to let public visitors comment too), people pin comments on the site's pages.
To act on them, export the open threads with their page URLs, pins and replies:
`npx -y https://taslak.dev/cli/taslak.tgz comments export` (Markdown; `--json` for JSON), the
`export_comments` MCP tool, or `GET /v1/spaces/{space_id}/comments/export`. Resolve a thread once
it is handled (`comments resolve <id>`, `resolve_comment`, or `PATCH` with `{"resolved": true}`);
the people in it are e-mailed. A visual review (`review create <version>`, `create_visual_review`)
opens a one-hour link to one version for feedback; its screenshot may be unavailable, so use the
link. With comments off, public visitors see the page unchanged.

## A database for the site

Each space can have a SQLite database (`taslak db create`, or `db_create` / `db_schema` in MCP).
Create tables with `taslak db sql "CREATE TABLE messages (name TEXT, body TEXT)"` or the
`db_query` tool, then decide what the published site may do with each table:
`taslak db rules messages public public none` (read, insert, edit: `none`, `session` for
share-link, password and preview visitors, or `public`). The site uses it from the browser:

```html
<script src="/__taslak/db.js"></script>
<script>
  const messages = taslak.db.table("messages");
  await messages.insert({ name: "Ayşe", body: "Merhaba" });
  const latest = await messages.list({ order: "-_id", limit: 20 });   // where: { name: "Ayşe" }
</script>
```

Forms open to everyone attract spam. `taslak db rules messages none public none --captcha --notify`
makes visitors pass a Cloudflare Turnstile check before inserting (db.js shows it and retries by
itself, also on custom domains) and emails the team owners about every new row; the
`row.inserted` webhook carries the row to other systems.

Rows carry their row id as `_id`; `update(id, patch)` and `delete(id)` need the edit rule. Sizes per
database: 100 MB on Free, 1 GB on Go, 10 GB on Plus.

## Files for the site

Each space can store files apart from its published versions, such as photos sent with a form.
Your team uploads with `npx -y https://taslak.dev/cli/taslak.tgz storage upload photo.jpg --folder photos`
(`storage list`, `storage delete <id>`) or `POST /v1/spaces/{space_id}/storage` (multipart fields
`folder` and `file`, up to 50 MB). Files sit in folders (`GET/POST/PATCH/DELETE
/v1/spaces/{space_id}/storage/folders`); renaming or moving a file (`PATCH .../storage/{id}`) keeps
its address. Visitors upload from the site once the team allows it
(`PUT /v1/spaces/{space_id}/storage/rules` with `{"uploads":"public","captcha":true}`):

```html
<script src="/__taslak/sdk.js"></script>
<script>
  const file = await taslak.storage.upload(input.files[0], { folder: "photos" });   // { id, url, content_type, size }
  await taslak.db.table("photos").insert({ url: file.url });
</script>
```

Visitor files are at most 5 MB, 200 MB a day per site. Web pages, scripts, SVG images and programs are
refused. Every file is served from `/__taslak/u/<id>?k=<key>`; anyone with that address can open
it, so do not store secrets. Files count toward the team's plan storage.

## Project settings: taslak.jsonc

`npx -y https://taslak.dev/cli/taslak.tgz init` writes a `taslak.jsonc` next to your project; the CLI
reads it from the published folder or its nearest parent and never publishes it. Schema:
https://taslak.dev/schemas/taslak.json.

```jsonc
{
  "$schema": "https://taslak.dev/schemas/taslak.json",
  "space": "spc_…",
  "build": { "command": "npm run build", "output": "dist" },
  "meta": { "title": "My site", "description": "…", "image": "/og.png", "favicon": "/favicon.ico" },
  "theme": { "name": "My site", "logo": "/logo.svg", "accent": "#2563eb" },
  "analytics": { "googleAnalytics": "G-XXXXXXX" },
  "listing": true,
  "redirects": [{ "from": "/old", "to": "/new", "status": 301 }],
  "headers": [{ "path": "/assets/*", "headers": { "Cache-Control": "public, max-age=31536000, immutable" } }]
}
```

`meta` fills sharing tags on pages that lack them; `theme` styles the pages Taslak renders (password,
private, not found, waiting, folder listings); `analytics` adds tags to every HTML page. The same
settings live on the site's Appearance page (`PUT /v1/spaces/{space_id}/appearance`), which wins
field by field and applies without a publish.

## Site behaviour

- `/about` serves `/about/index.html` (redirecting to `/about/`) or `/about.html`.
- `/404.html` is the not-found page.
- `_redirects` and `_headers` in the site root work like Netlify's: `/old /new 301`,
  `/blog/* /posts/:splat 302`, `/* /index.html 200` for single-page apps. A line is
  `from [name=:value …] to [status[!]] [conditions]`:
  - `/api/* https://api.example.com/:splat 200` proxies to another server (any method);
  - `/search q=:term /results?q=:term 302` matches the query string;
  - conditions `Country=TR,DE`, `Language=tr`, `Cookie=session` and `Agent=bot|ai|human` must all
    hold, and such answers are never cached for others;
  - `!` after the status applies the rule even when a file exists at the path.
- A site lives at `https://<slug>.taslak.page`; each version also at `https://v<number>--<slug>.taslak.page`.
- Custom domains: `GET /v1/domains/preflight?team_id=…&hostname=…` says whether a hostname can be
  added and which records it needs. `POST /v1/spaces/{space_id}/domains` with
  `{"hostname":"www.example.com"}` returns the CNAME and TXT records to add. For providers that
  only accept IP addresses, or a wildcard such as `*.example.com` (Plus), use
  `"mode":"nameservers"` and point the domain's nameservers to the returned ones. Domains belong
  to the team and move between its sites (`PATCH /v1/domains/{hostname}`); when one stays
  pending, `GET /v1/domains/{hostname}/diagnostics` says which record to fix.

## More

Every docs page is Markdown at `https://taslak.dev/docs/<page>.md`; the list is in
https://taslak.dev/llms.txt.

- CLI reference: https://taslak.dev/docs/cli.md
- MCP tools and agent permissions: https://taslak.dev/docs/agents.md
- API guide: https://taslak.dev/docs/api.md
- Partners, customer tenants and tenant tokens: https://taslak.dev/docs/partners.md
- taslak.jsonc: https://taslak.dev/docs/config.md
- Framework recipes: https://taslak.dev/docs/frameworks.md
- Limits: https://taslak.dev/docs/limits.md
- Error codes: https://taslak.dev/docs/errors.md (every API error's `code` is listed there)
