/horizon-device
Manage Horizon native VNC Device panels and drive isolated local desktops for simulators and native application tests through device_panel and the horizon-device CLI/MCP. Device panels work on every platform; driving an isolated desktop works on Linux with X11 only. Browser
Claude Code
$ npx -y skills add peters/horizon --skill horizon-device --agent claude-code
Codex
$ npx skills add peters/horizon --skill horizon-device -a codex -y
Cursor
$ npx skills add peters/horizon --skill horizon-device -a cursor -y
Gemini CLI
$ npx skills add peters/horizon --skill horizon-device -a gemini-cli -y
GitHub Copilot
$ npx skills add peters/horizon --skill horizon-device -a github-copilot -y
OpenCode
$ npx skills add peters/horizon --skill horizon-device -a opencode -y
Windsurf
$ npx skills add peters/horizon --skill horizon-device -a windsurf -y
SKILL.md
horizon-device.SKILL.mdname: horizon-device
description: Manage Horizon native VNC Device panels and drive isolated local desktops for simulators and native application tests through device_panel and the horizon-device CLI/MCP. Device panels work on every platform; driving an isolated desktop works on Linux with X11 only. Browser pages use horizon-browser.
Horizon native VNC and device control
Use this skill for Horizon VNC Device panels, simulators, and isolated native application tests. Browser pages use the `horizon-browser` skill and `browser_*` tools. Never drive a browser with device input.
Native VNC Device panel
Always observe interactive native tests live through a **Horizon native VNC Device panel in the user's current workspace**. Use a task-owned isolated desktop and private application state. Do not use noVNC or a browser viewer, or substitute screenshots or recordings for the live panel.
`device_panel` operations are `create`, `list`, `inspect`, `screenshot`, `video`, `visibility`, `reveal`, `reconnect`, and `close`. Use the public `device_panel` MCP tool for viewer lifecycle. Call `operation: "list"` to discover panels in the caller's workspace. Create a task-owned viewer with `operation: "create"` and the fixture's numeric loopback `endpoint` (IP and nonzero port); retain its returned `panel_id`. This requires a Horizon-launched agent and a supporting host. The tool does not provision a desktop or forward input. The source checkout's `scripts/device-smoke/README.md` describes the isolated fixture; launch it with `--native-view` and use its `vnc_address`, not a browser URL.
A VNC server on another machine's loopback is reached with optional `ssh` in create: `{ "host": "lab", "user": "deploy", "port": 2222 }` (user and port optional). `endpoint` is then the address as seen from that SSH host, typically `127.0.0.1:5900`. Horizon runs `ssh -W` with its own SSH configuration and keys, with strict host-key checking pinned regardless of `ssh_config`, so the host must already be trusted in `known_hosts`; never pass credentials, key paths or ssh options. Labels may only contain letters, digits, `.`, `_`, `-` (and `:` in an IPv6 host), at most 253 characters for the host and 64 for the user, and are refused otherwise, so nothing reaches ssh as an option or a shell fragment. Inspect and list report `ssh` for tunnelled viewers, and `connection_error` carries ssh's last lines when the tunnel fails.
An untrusted host is a refusal, not an unreachable host. When the tunnel fails that way, inspect returns `connection_failure`: `code: "host_not_trusted"` when ssh holds no `known_hosts` entry, or `code: "host_key_changed"` when `known_hosts` holds a different key. It also carries `ssh_host_key` with the `host` and `port`, `ssh_host_key.observed` with the `key_type` and `sha256` fingerprint that the host presented to a probe, and `action`, which is what the person does. The probe repeats the tunnel's own ssh configuration, so the fingerprint is the key that ssh saw; `observed` is absent when a `ProxyJump`, a `ProxyCommand` or a `HostKeyAlias` keeps a direct probe from repeating that connection, and `action` then says which one applies. Do not retry either code; they need a person. Report the host, the observed fingerprint and the action, and say that a host key must be compared with the machine out of band before it is added. Never accept a host key, write `known_hosts` yourself, or claim that a probe proves which machine answered. When the person reports that the host is trusted, call `reconnect` and inspect again. Without a code, `connection_error` alone stands and the failure may be a network or ssh configuration problem.
When known, include optional `identity` in create: `machine_name`, `hostname`, `ip_addresses` (numeric IPv4/IPv6 list), and `tailscale_name`. These are labels supplied by the session creator, not verified identity. Use details for the actual target machine; never substitute the local tunnel endpoint or the viewer's own hostname. Omit unknown fields. Inspect/list return these labels.
Creation returns immediately. Use `operation: "inspect"` and the returned id to check `connection: "connected"`. For a visible, on-screen viewer, also verify `image_received`, `image_displayed` and an advancing `frame_sequence` while target output changes. `visible` is only a presentation setting; an image can be off canvas or clipped. Current hosts keep uploading about once a second while a viewer is not drawn, so `frame_sequence` keeps advancing off canvas; older hosts pause uploads there, so also accept an advancing `received_frame_sequence` while target output changes as reception evidence. That counter tracks received image updates independently of the uploaded-image `frame_sequence`. Establish live presentation once per connection (`image_displayed` with advancing frames). If the viewer later reports `not_rendered` with `outside_canvas` while `last_displayed_age_millis` is present, the person navigated away: keep testing and recording, do not pause the lane, and do not reveal again just to advance counters. Both counters reset on reconnect and neither is a heartbeat: a stationary desktop is not a connection failure. Older hosts may omit reception progress; do not interpret a missing/default-zero counter as a failure. Set `operation: "visibility", visible: true` for a hidden owned viewer, then verify actual presentation. Never claim that a separate isolated viewer is visible to the user merely because its screenshot is available.
Check this automatically; never ask a human to confirm visibility or advancing frames. Save at least three timestamped public inspections, two seconds apart. The current `frame_sequence` counts uploaded images, not independent transport heartbeats. An unchanged sequence on a static or unknown target does not prove a stalled connection. A displayed static image proves presentation only; changing target output and sequence advancement are needed for live-motion ev
Read more
name: horizon-device description: Manage Horizon native VNC Device panels and drive isolated local desktops for simulators and native application tests through device_panel and the horizon-device CLI/MCP. Device panels work on every platform; driving an isolated desktop works on Linux with X11 only. Browser pages use horizon-browser.
Horizon native VNC and device control
Use this skill for Horizon VNC Device panels, simulators, and isolated native application tests. Browser pages use the `horizon-browser` skill and `browser_*` tools. Never drive a browser with device input.
Native VNC Device panel
Always observe interactive native tests live through a **Horizon native VNC Device panel in the user's current workspace**. Use a task-owned isolated desktop and private application state. Do not use noVNC or a browser viewer, or substitute screenshots or recordings for the live panel.
`device_panel` operations are `create`, `list`, `inspect`, `screenshot`, `video`, `visibility`, `reveal`, `reconnect`, and `close`. Use the public `device_panel` MCP tool for viewer lifecycle. Call `operation: "list"` to discover panels in the caller's workspace. Create a task-owned viewer with `operation: "create"` and the fixture's numeric loopback `endpoint` (IP and nonzero port); retain its returned `panel_id`. This requires a Horizon-launched agent and a supporting host. The tool does not provision a desktop or forward input. The source checkout's `scripts/device-smoke/README.md` describes the isolated fixture; launch it with `--native-view` and use its `vnc_address`, not a browser URL.
A VNC server on another machine's loopback is reached with optional `ssh` in create: `{ "host": "lab", "user": "deploy", "port": 2222 }` (user and port optional). `endpoint` is then the address as seen from that SSH host, typically `127.0.0.1:5900`. Horizon runs `ssh -W` with its own SSH configuration and keys, with strict host-key checking pinned regardless of `ssh_config`, so the host must already be trusted in `known_hosts`; never pass credentials, key paths or ssh options. Labels may only contain letters, digits, `.`, `_`, `-` (and `:` in an IPv6 host), at most 253 characters for the host and 64 for the user, and are refused otherwise, so nothing reaches ssh as an option or a shell fragment. Inspect and list report `ssh` for tunnelled viewers, and `connection_error` carries ssh's last lines when the tunnel fails.
An untrusted host is a refusal, not an unreachable host. When the tunnel fails that way, inspect returns `connection_failure`: `code: "host_not_trusted"` when ssh holds no `known_hosts` entry, or `code: "host_key_changed"` when `known_hosts` holds a different key. It also carries `ssh_host_key` with the `host` and `port`, `ssh_host_key.observed` with the `key_type` and `sha256` fingerprint that the host presented to a probe, and `action`, which is what the person does. The probe repeats the tunnel's own ssh configuration, so the fingerprint is the key that ssh saw; `observed` is absent when a `ProxyJump`, a `ProxyCommand` or a `HostKeyAlias` keeps a direct probe from repeating that connection, and `action` then says which one applies. Do not retry either code; they need a person. Report the host, the observed fingerprint and the action, and say that a host key must be compared with the machine out of band before it is added. Never accept a host key, write `known_hosts` yourself, or claim that a probe proves which machine answered. When the person reports that the host is trusted, call `reconnect` and inspect again. Without a code, `connection_error` alone stands and the failure may be a network or ssh configuration problem.
When known, include optional `identity` in create: `machine_name`, `hostname`, `ip_addresses` (numeric IPv4/IPv6 list), and `tailscale_name`. These are labels supplied by the session creator, not verified identity. Use details for the actual target machine; never substitute the local tunnel endpoint or the viewer's own hostname. Omit unknown fields. Inspect/list return these labels.
Creation returns immediately. Use `operation: "inspect"` and the returned id to check `connection: "connected"`. For a visible, on-screen viewer, also verify `image_received`, `image_displayed` and an advancing `frame_sequence` while target output changes. `visible` is only a presentation setting; an image can be off canvas or clipped. Current hosts keep uploading about once a second while a viewer is not drawn, so `frame_sequence` keeps advancing off canvas; older hosts pause uploads there, so also accept an advancing `received_frame_sequence` while target output changes as reception evidence. That counter tracks received image updates independently of the uploaded-image `frame_sequence`. Establish live presentation once per connection (`image_displayed` with advancing frames). If the viewer later reports `not_rendered` with `outside_canvas` while `last_displayed_age_millis` is present, the person navigated away: keep testing and recording, do not pause the lane, and do not reveal again just to advance counters. Both counters reset on reconnect and neither is a heartbeat: a stationary desktop is not a connection failure. Older hosts may omit reception progress; do not interpret a missing/default-zero counter as a failure. Set `operation: "visibility", visible: true` for a hidden owned viewer, then verify actual presentation. Never claim that a separate isolated viewer is visible to the user merely because its screenshot is available.
Check this automatically; never ask a human to confirm visibility or advancing frames. Save at least three timestamped public inspections, two seconds apart. The current `frame_sequence` counts uploaded images, not independent transport heartbeats. An unchanged sequence on a static or unknown target does not prove a stalled connection. A displayed static image proves presentation only; changing target output and sequence advancement are needed for live-motion ev
Other skills on horizon.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about horizon-device.
- Which coding agents can use horizon-device?
- horizon-device installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

