Privacy
Short version: anonymous usage counts, no cookies, no recordings, never your project content. Every part of it can be turned off.
The website (cozyclay.org)
The site uses PostHog (US Cloud) for anonymous usage analytics and Cloudflare Web Analytics for page counts. There are no cookies and no session recording, and Do-Not-Track is respected. PostHog keeps a random pseudonymous identifier in your browser's localStorage so returning visits can be counted; it is never linked to an account or to anything you make, and clearing site data removes it. Country comes from ingest-time GeoIP only; no precise location is collected.
The in-browser playground on the home page runs the studio inside the page. It does not read or write your saved scenes, and nothing you build there leaves your browser unless you download it.
The npm package (npx cozyclay)
The official npm package measures anonymous first launches, sessions and an in-app usage funnel on
its local studio. It stores one random installation identifier in ~/.config/cozyclay/state.json
(or $XDG_CONFIG_HOME/cozyclay/state.json) so returning use can be counted across normal
CLI restarts, ports and browser storage resets. The browser's memory-only SDK starts with this
anonymous ID on each launch. Independent state files receive independent random IDs, without
fingerprinting, account linking or cross-device matching; do not copy the state file between users.
A copied start command such as npx cozyclay --via site stores that one source label
with the first-launch receipt instead of asking where you heard about CozyClay.
Prompt text, asset names,
file names, project content, local paths and any user-entered text are never collected. For a prompt sent to the agent, only two values derived from its text leave the device: a length bucket and a similarity grade (first, same, near or different) that the browser computes against the previous prompt. Neither the text nor its words are sent; the previous prompt is kept in the page only as its set of words, until the page closes. Session
duration and action counts are bucketed. Source checkouts, forks, development servers, CI and tests
send nothing.
The package prints this disclosure once on first launch. Control it at any time:
cclay telemetry status
cclay telemetry off
cclay telemetry on
COZYCLAY_TELEMETRY=0 or DO_NOT_TRACK=1 disable collection for a launch. The
topbar toggle inside the studio changes the same setting and removes the installation identifier.
Enabling collection later creates a new, unlinked ID; the original first-launch receipt remains,
so this does not create another new-install event. Environment overrides only suspend collection
and do not erase the stored ID. Opting out also suppresses the session-end beacon.
Explicit internal and QA marking
Internal marking is optional and never enables telemetry. Use
cclay telemetry internal on or cclay telemetry internal off to store
the boolean internalQa in the npm CLI state file, then reload open studio tabs.
For the hosted app, open https://cozyclay.org/app/?internal_qa=1 to mark that browser
or ?internal_qa=0 to clear it. A single explicit 1 or 0 persists as
cozyclay.internalQa in localStorage for that origin. The npm app uses only its CLI
setting, not this URL parameter.
All app events and end beacons carry boolean internal_qa, false by default.
External usage queries exclude only explicitly marked events; localhost, ports, country and
source checkouts are never used to infer internal people. Unmarked local-app users remain
included. Opt-out, Do-Not-Track, CI, origin restrictions and disabled builds still win.
Historical unmarked QA cannot be retroactively distinguished.
Events collected
| Event | Purpose |
|---|---|
| install:first_launch, app:session_started, app:session_ended | Counting installs and sessions; duration and action counts are bucketed. install:first_launch may carry heard_from (x, hn, reddit, github, friend, other, site or playground), from the first-launch prompt or a copied npx cozyclay --via command. app:session_ended also carries scenes_touched (how many scenes were created or loaded in the session, an exact count capped at 20, not bucketed), session_id (the opaque random session id the PostHog SDK generates, used only to join a session's last events, such as an error just before it ended) and per-session edit gesture counts in five groups, pose_edit_bucket, camera_edit_bucket, object_edit_bucket, shot_edit_bucket and prompt_edit_bucket, each bucketed (0, 1-3, 4-10 or gte11); never what was edited |
| app:friction_summary | Once per session, next to app:session_ended: how much friction the session had, as closed buckets only. active_time (the session duration bucket); counts of refusals the Studio gave the user (refusals_ui) and the agent (refusals_agent), user undos (undos), errors (errors), browser long tasks (long_tasks) and motion compiles (compiles), each as 0, 1, 2, 3, 4-5, 6-10 or gte11; the longest long task (long_task_max: lt50, 50-150, 150-500, 500-2000 or gte2000 ms); the share of late playback ticks (late_tick_ratio: 0, lt5, 5-20, 20-50 or gte50 %). rage_clicks and dead_clicks count every ui:rage_click and ui:dead_click, shortcut_ignored every shortcut:ignored, search_empty every search:no_results, and nav_thrash and camera_thrash every nav:thrash and camera:thrash, including those past their event's cap. Never which command, control, key or text was involved. |
| studio:refused | A command the Studio refused, as closed values only: code (one of the Studio's fixed error codes, such as INVALID_ARGUMENT, STALE_SCENE or TARGET_BUSY, else other), origin (ui for the user's own control, agent for the agent or an MCP client), phase (admission, refused before any change, or commit, refused after it started changing) and group (the command's family: object, shot, cast, scene, motion, edit, task, agent or other). At most 3 per code, group and origin per session; the rest are only counted in app:friction_summary. A cancel the caller asked for is not a refusal. Never the command's arguments, target or message. |
| edit:undone | A user undo (agent undos are not counted): age (lt2s, 2-10s, 10-60s or gte60s since the undone edit was made, only when the undo was done and its age is known), kind (pose, camera, object, shot, prompt, agent_turn for the agent's whole turn, ik_preview for an un-applied IK drag, or other), via (key, menu or button) and result (done, noop when there was nothing to undo, or refused). At most 20 per session. Never what was undone. |
| agent:turn_summary | One per agent turn: turn_id (the same random id as agent:turn_*); outcome (succeeded, failed or cancelled); wall (the turn's length) and first_tool (time to the first tool call, absent without tools), each lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m; counts of tool calls (tool_calls), failed tool calls (tool_failed), tool calls the Studio refused (refused), commands re-sent once after a stale revision or a busy gesture (stale_retry) and motion compiles (drafts), each as 0, 1, 2, 3, 4-5, 6-10 or gte11; refusal_top (the most frequent refusal code, or none); authored (true or false: the turn changed the scene); qa_first and qa_last (clean or problems, absent without drafts); qa_trend (improved, flat, worse, or na with fewer than 2 drafts); stuck (true or false: the motion compiler's stop rule fired); cancel_phase (before_tool, during_tool, after_tool or na). No prompt text, tool names, messages or responses. |
| agent:post_turn_edit | One per agent turn: turn_id (the same random id) and the user's own edit gestures in the 120 seconds after the turn ended, closed early when the next turn starts or the page closes. edited (true or false); counts per group (pose, camera, object, shot, and prompt_block, which counts edits to a take's prompt and take fields), each as 0, 1, 2, 3, 4-5, 6-10 or gte11, where one continuous drag is one gesture; first_delay (time to the first edit: lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m), only when edited is true. Never which object or what changed. |
| motion:draft_compiled | One per motion compile (each draft the agent's motion work produces), at most 50 per session; the rest only move app:friction_summary compiles. draft_index (the draft's number for its character in the turn) and qa_problems as 0, 1, 2, 3, 4-5, 6-10 or gte11; qa_clean; qa_frames (frame-level QA issues: 0, 1-10, 11-50, 51-200 or gte201); compile (the whole compile, QA included: lt50, 50-150, 150-500, 500-2000 or gte2000 ms); installed, stuck, repaired, searched and adjusted (true or false); frames (take length: lt30, 30-120, 121-480 or gte481). Never the motion spec, the take, skill names or QA text. |
| shortcut:ignored | A bound Studio key that did nothing, as closed values only: key_class (undo, redo, mode, transform, frame, duplicate or delete) and reason (no_selection, wrong_mode, workflow_mode, busy or empty_history). At most 5 per key_class per session; the rest only move app:friction_summary shortcut_ignored. Keys typed into a text field, a list box or an editable area, and keys of an input-method composition, are never looked at. Never the key character. |
| shortcut:unbound | A common key combination the Studio has no binding for, pressed outside any text field: combo (copy, paste, cut, select_all, group, find, rename or help). At most 3 per combo per session. Never the key character. |
| help:shortcuts_opened | Help > Keyboard shortcuts was opened. No properties. At most 3 per session. |
| search:no_results | A search in the Content browser (surface assets) or the Outliner (outliner) that has shown nothing for 1 second: surface and query_len (the length of the search, 1, 2-3, 4-8 or gte9 characters). At most 3 per surface per session; every one moves app:friction_summary search_empty. Never the search text. |
| nav:thrash | 4 or more switches of one panel or mode within 10 seconds with no edit in between: surface (dock_tab, mode or agent_panel), switches (4-5, 6-10 or gte11) and window_s (always 10). At most one per surface every 30 seconds; every one moves app:friction_summary nav_thrash. Never which tab or mode. |
| camera:thrash | 6 or more viewport camera moves (orbit, fly or pan drags) within 20 seconds with no edit in between: gestures (6-10 or gte11), kind_mix (orbit, fly, pan or mixed) and window_s (always 20). At most one every 30 seconds; every one moves app:friction_summary camera_thrash. Never a camera position or view. |
| agent:connect_opened | The model-connect list opened: entry (first_run, failure_card, signed_out, menu or other; the signed-out card reads first_run until this browser profile has connected once). At most 5 per session. |
| agent:connect_started | One model-connect attempt began: provider (one of the providers CozyClay configures — openai-codex, anthropic, anthropic-subscription, openai, google, minimax, minimax-cn, openrouter, cliproxy, openai-compatible — or other) and method (oauth, api_key or chatgpt). |
| agent:connect_finished | That attempt ended: provider, method, outcome (ok, failed, cancelled with the Cancel button, abandoned when closed or left unfinished), step it had reached (list, prompt, waiting or done) and duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). Never the key, token, account, email or error text. |
| agent:model_changed | A model picked in the agent's model list: its provider (as above, or other) and after_failure (whether the last agent turn had failed). At most 10 per session. |
| agent:prompt_sent | One per prompt sent to the agent: turn_id (the same random id as agent:turn_*); provider (as for agent:connect_started, or other); via (typed, retry_button, regenerate, chip or supervisor); prompt_len (1-20, 21-80, 81-240 or gte241 characters); compose_ms (first keystroke to send, typed prompts only) and gap (since the previous turn ended, or first) as lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m; similarity (first, same, near or different, computed in the browser against the previous prompt's words); prev_outcome (succeeded, failed, cancelled or none); turn_index (1, 2, 3, 4-5, 6-10 or gte11); attached and task_mode (true or false). Never the prompt text or its words. |
| ui:rage_click | Three or more fast clicks (within 1 second and 24 pixels) on one primary control: control (a control id from a fixed list, such as tl_play or export_video, or other), clicks (3, 4-5 or gte6) and changed (whether the Studio responded within half a second). At most 2 per control per session. No coordinates, no text, no element or object ids. |
| ui:dead_click | A click on a primary control that did nothing: control (as above) and reason (aria_disabled when the control was disabled, refused when the Studio refused the command, no_effect when nothing changed within half a second). At most 2 per control per session. Clicks into text fields are never observed. |
| feature:used | One signal per feature per session: a fixed feature name such as pose_edit (a real bone or pose change), pose_save (saving a pose to the library), camera_fly, export_video, export_storyboard or export_render_pass |
| $pageview, scene:created, scene:loaded, craft:first_action, activation:completed | Funnel and drop-off analysis |
| craft:first_edit | First successful Studio authoring change per editor mount; only a fixed edit-kind label and definition version 2 (not camera navigation, playback, loading, restore, or undo/redo) |
| playground:first_edit | The same versioned first-edit definition for Playground, counted separately from Studio; no prompts or scene contents |
| project:saved, project:opened | Whether people keep their work (object counts and ages are bucketed; never names or paths) |
| workflow:run_requested | Workflow execution requests: surface (workflow), ephemeral run_id and node_count_bucket (0, 1-3, 4-10 or gte11). No graph contents, node labels or outputs. |
| workflow:run_succeeded, workflow:run_failed, workflow:run_cancelled | Workflow outcomes paired by run_id, with duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, capture_failed, generation_failed or unknown). Starting another run does not imply cancellation. |
| workflow:result_applied | Captured or generated image/video output was written to the Workflow canvas: run_id only, at most once per run, independently of a later failure. No image bytes or output contents. |
| agent:turn_requested | Explicit Agent turn requests from surface (studio or workflow), paired by ephemeral turn_id. No prompts, responses or model identifiers. |
| agent:tool_executed | Agent tool observations: turn_id, tool_category (workflow_read, workflow_write, workflow_run, frame_capture, image_generate, scene_write or other), outcome (succeeded, failed or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Tool names, arguments and results are excluded. |
| agent:turn_succeeded, agent:turn_failed, agent:turn_cancelled | Agent turn outcomes: turn_id and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, auth, rate_limited, tool_failed, upstream, key_refused, stale_scene, capability_missing, busy, invalid_request or unknown). auth means CozyClay's local agent has no sign-in or refused the session; key_refused means the model provider rejected the key or credentials; stale_scene means the turn was refused because the scene changed underneath it; capability_missing means a required capability was unavailable, for example the live hub port was already in use; busy means another turn or target was busy; invalid_request means the request itself was rejected. Missing terminal delivery is not success. |
| agent:result_applied | An Agent turn produced an acknowledged editor change: turn_id only, at most once per turn. Successful reads and focus/navigation do not prove applied output. |
| mcp:tool_requested | Connected-editor MCP execution requests: ephemeral request_id and tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other). |
| mcp:tool_executed | Connected-editor MCP outcomes: request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other), outcome (succeeded, failed, uncertain or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Control-plane success does not prove a mutation applied. |
| mcp:result_applied | A connected editor acknowledged an MCP mutation, paired by request_id. Detached or disconnected MCP sessions remain unobserved. |
| export:blocking_frame_succeeded, export:video_succeeded | Export reliability |
| export:attempt_started, export:attempt_succeeded, export:attempt_failed, export:attempt_cancelled | Export attempts and outcomes, paired by an ephemeral random attempt_id; export_kind (video, depth_video, frame, keyframe_pack, contact_sheet, panel_pack, animatic, animation_project, workflow_send, storyboard, render_pass or otio), format (mp4, png, zip, cclayproject or otio), initiating surface (studio, workflow or embed), is_retry (whether the attempt retries an earlier one), retry_of (the retried attempt's attempt_id, only when is_retry is true) and objects (the scene's object count, bucketed 0, 1-3, 4-10 or gte11). Terminal events include a bucketed duration_bucket; failures and real cancellations include a normalized failure_code (unsupported_codec, encode_failed, render_failed, aborted or unknown), the stage where the export stopped (preparing, encoding, finalizing or handoff) and a progress bucket (0, 1-25, 26-50, 51-75 or 76-99 percent), never raw error messages, filenames, URLs, paths, prompts or scene content. Success means pipeline completion or download handoff, not confirmation that the OS saved a file. |
| export:saved | How each exported file was saved, one event per file: method (workspace, written into the export folder you chose, or download, a browser download) and reason (none; unsupported, the browser has no folder access; no_workspace, no export folder is chosen; permission, folder access was denied; or write_failed, the folder write failed and the file fell back to a download). Never the file name or path. |
| export:keyframe_pack | Legacy Workflow pack handoffs, with numeric entries count and source=workflow; not an additional export attempt |
| sculpt_run | One event per finished Sculpt mode run: outcome (done, cancelled or failed), rounds (0 to 10), final_iou (how closely the final sculpt matched the reference image, 0 to 1 rounded to two decimals; omitted when the run was never scored), duration_ms (elapsed run time in milliseconds) and had_target (whether the run reworked an existing sculpt instead of creating one). Numbers only: never the reference image, recipe, part names or sculpt id. |
| playground:opened, playground:first_action | Whether the home-page playground gets used |
| app:error | Uncaught errors and unhandled promise rejections, at most ten distinct per session: error_kind (error or unhandled_rejection), error_type (a built-in name such as TypeError or DOMException, other or non_error), error_source (app, extension, external or unknown) and, only for the app's own scripts, error_file (the bundle file name), error_line and error_col. Never the error message, stack text, URLs, query strings, paths, prompts or scene content. |
| app:render_crash | A render error that brought up the studio's crash screen, at most two per session: error_type (the same closed labels as app:error). Never the error message, stack text, component names, file names or scene content. |
| app:gl_lost | The 3D view's graphics failed, at most once per kind per session: kind (unavailable when WebGL is off at startup, context_lost when the GPU context drops later). Never the GPU renderer string. |
| studio:ready | Once per session: how long the studio took to show its first frame, from page navigation to the first frame after the first render: boot (lt2s, 2-5s, 5-15s or gte15s). Never what was loaded. |
| playback:summary | One per playback run (play until stop, or until the frame rate or speed changes), at most 10 per session: duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m); late_ticks, an estimate of dropped frames, not a measured one: the share of playback timer ticks that had to catch up more than one frame (0, lt5, 5-20, 20-50 or gte50 %); max_catchup (the most frames one tick caught up: 1, 2, 3-5 or gte6); fps (12, 24, 30, 60 or other); speed (0.5, 1, 2 or other); measured_fps (the browser's frame rate during playback, the device:profile frame-rate buckets). Every run also feeds app:friction_summary.late_tick_ratio. Never the scene, the take or the frame range. |
| guide:opened | The first-60-seconds guide opened, at most once per session: mode (animation or storyboard). Never step text or what you did. |
| guide:step | A guide step marked done, each step at most once per session: mode (animation or storyboard), step (1 to 4) and step_ms (time on that step: lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). |
| guide:dismissed | The guide closed before its last step, at most once per session: mode (animation or storyboard) and reached (1 to 4, the step it was showing). |
| guide:completed | The guide's last step marked done, at most once per session: mode (animation or storyboard). |
| agent:panel_toggled | The agent panel opened or closed, at most 10 per session: open (true or false) and via (key for Cmd/Ctrl+B, button for the toolbar control). Never what the panel showed. |
| object:deleted_soon | You deleted an object you added in this session, at most 10 per session: age (how long after adding it: lt2s, 2-10s, 10-60s or gte60s). Object ids are reused names, so this is an estimate. Never the object's id, name or kind. |
| object:delete_undone | You undid an object deletion within its 7-second undo window, at most 10 per session: age (time from the deletion to the undo: lt2s or 2-10s). Never the object's id, name or kind. |
| update:notice_shown | The update notice appeared in the top bar of an npm studio, once per session: kind (global or npx) and behind (patch, minor or major: how far the running version is behind the newest release). Never a path or a command |
| update:clicked | A button on the update notice, at most 10 per session: action (update_now, copy_command, later, dismiss or release_notes) and kind (global or npx) |
| update:finished | A one-click update ended, at most 3 per session: outcome (ok or failed), error_class (none, permission, network, node_version, npm_missing or other) and duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). Never npm's output, a path or the command line |
| device:profile | Once per session, whether the studio can run smoothly: gpu_vendor (nvidia, amd, intel, apple, qualcomm, arm, software, other or unknown), gpu_class (discrete, integrated, software or unknown), webgl (webgl2, webgl1 or none), cpu_bucket (1-4, 5-8, 9-16, gte17 or unknown), memory_bucket (lt4, 4-7, gte8 or unknown) and frame_rate_bucket (lt20, 20-40, 40-55, gte55 or unknown) from one three-second sample. The GPU renderer string and exact hardware values never leave the browser. |
| survey:use_case | An optional question shown at most once after a successful export, only while telemetry is on: use_case (animation, film, game, ad_mv, personal, other or skip) and team (team, solo or skip). Closed choices only; no free text. |
| hosted:composer_viewed, hosted:login_started, hosted:ticket_created, hosted:result_opened, hosted:opened_in_studio | Hosted demo funnel |
Only the events in this table are sent; any other event name is dropped in the browser before it reaches PostHog. Every regular app event and session-end beacon carries the same distribution (npm or hosted), app_version when available, origin_kind (local or hosted), internal_qa boolean, coarse operating-system label and, for npm sessions, install_kind (npx or global) and update_status (latest, outdated or unknown), read from the package's own cached update check. The update notice asks the launcher on 127.0.0.1 whether a newer release exists; the launcher answers from that same cached check, so the notice adds no request to anyone else. Update now runs npm install -g cozyclay@<version> on your machine, and only when you press it. PostHog's free plan retains events for one year.
Workflow run_id, Agent turn_id and MCP request_id values are fresh random 32-character hexadecimal values held only for one execution. Execution telemetry contains only closed categories, outcomes and elapsed-time buckets; missing telemetry is left unknown, and a disconnected MCP editor is not backfilled.
The hosted demo (Google sign-in)
The hosted demo is not open to the public right now. This section describes what it handled while it was running. The hosted motion demo asks you to sign in with Google so that the daily generation cap can be enforced per account. It receives your Google account's email address and basic profile only, stores a session cookie signed by our server, and keeps your prompt and the generated result for up to 30 days so the ticket link keeps working. Results are stored on Cloudflare R2 and deleted after that window. The demo does not post to your account or read anything else from it. Cloudflare Turnstile is used to block automated submissions.
Your rights and contact
Nothing collected is tied to a person, so there is nothing to look up by name; opting out and clearing site data is the complete remedy. For anything else, open an issue on GitHub or email doyunha16@gmail.com.