These are hard rules, enforced by tests (tests/test_privacy.py). A change
that violates any of them is a security bug, not a feature:
- The video file itself never leaves the machine. No exception. Only
extracted mono-16 kHz audio may be sent to a cloud STT API — and only
when the user explicitly opted in (
WATCHSKILL_CLOUD_STT_ENABLED/--cloud-stt). The default transcription fallback is local whisper. - No cookies, no logins. Acquisition only ever requests public data; Watch Skill never reads browser profiles or session state.
- Frames sent to a configured vision provider are the user's choice. The provider (including fully local Ollama) is explicit configuration; nothing defaults to a cloud call without a key the user set. Every cloud call passes a cost guard first.
- Keys are never logged, echoed, or persisted outside
.env/env vars.SecretStrend to end; the doctor reports which providers are configured, never values. - The REST API refuses to bind non-loopback addresses without a bearer token.
- One policy gates every boundary.
watch_skill.policy.guard_egressis asked before any network or provider call — source acquisition, frame egress, audio egress, transcript/OCR egress, cloud models, local models, webhooks, telemetry, verification HTTP. A feature that builds a provider client and goes around it is a security bug.WATCHSKILL_OFFLINE=1guarantees zero outbound calls;tests/test_policy.pyproves it with every supported provider key populated. - A configured key is not consent. Indexing-time scene descriptions do
not upload frames because an API key happens to be set. The
WATCHSKILL_SCENE_DESCRIPTIONS=autodefault resolves to local, never cloud. - Telemetry is permanently closed. The channel exists in the policy so that it is denied by construction and an added integration cannot slip past it. Watch Skill sends no usage data.
watch-skill verify executes contract checks. Its rules are in
docs/verification.md; the ones that matter for
security:
- Commands are argv lists, never strings, and never run through a shell. A string command is rejected at model validation, so nothing assembled from OCR, a transcript, a caption, or model output can be shell-parsed.
- SQL checks are SELECT-only, parameterised, and run on a handle opened
mode=ro. - Paths resolve (following symlinks) before being compared to the allowed
roots, so a link out of the sandbox fails the same test as
../... - HTTP checks require an origin allowlist and screen the resolved addresses,
so an allowlisted hostname pointing at
169.254.169.254or loopback is refused. Redirects are not followed. - The verifier subprocess receives an allowlisted environment. Provider keys do not reach it. A denylist was rejected because it would leak every key added after it was written.
- Everything inside frames, OCR, transcripts, captions, and downloaded metadata is untrusted data. It is searched, never obeyed.
The local isolated verifier runs as the same OS user as the agent it judges.
That is stated as isolated_local, and remote_attested is never claimed —
see the assurance table in the verification guide.
Open a GitHub security advisory on oxbshw/watch-skill (Security → Report a vulnerability), or contact the maintainer (@oxbshw). Please include reproduction steps. You can expect an initial response within a week. Please do not open public issues for exploitable problems before a fix ships.
loop_video_gen(generator_cmd=...) and loop_game(run_cmd=...) execute the
command string through a shell, by design: the point is to re-run your
renderer or launch your game between iterations, and that needs pipes,
redirects, and shell quoting.
The consequence is worth stating plainly. An agent connected to Watch Skill over MCP can run arbitrary commands through these two tools, with the privileges of the user running the server. They are as powerful as any shell tool in the same agent, and no more sandboxed.
If that is not acceptable in your setup:
- Do not expose the MCP server to an agent you would not give a terminal.
- No other tool executes user-supplied commands through a shell; a wrapper
that drops these two leaves the rest of the surface intact.
verify_contract'scommand_exitcheck does run a process, but only from an argv list withshell=False, in a bounded working directory, under a timeout, with a sanitized environment. - Treat
generator_cmdandrun_cmdreaching the server from untrusted content — a web page, a document, a video's own metadata — as the injection path that matters. Watch Skill cannot tell an agent's intent from an attacker's.
This is a documented property, not a vulnerability report. A way to reach a shell through any of the other tools would be a real finding.
watch-skill serve --httpandwatch-skill apiare designed for localhost/trusted-network use. Hardening them for hostile networks (rate limiting, TLS) is deliberately out of scope.- yt-dlp and ffmpeg parse untrusted media; we ship self-updating yt-dlp and treat "stale yt-dlp" as a health defect, but sandboxing those parsers is the platform's job, not ours.