EN / field notes OpenHands feature map

OpenHands / F18

Skills catalog

The Skills page (Customize → Skills, /skills) lists every skill the backend can load: the built-in catalog bundled with the frontend plus personal and project skills reported by the Agent Server. A user searches and filters the cards (filters are mirrored into the URL), opens a detail modal, switches skills on or off (saved to settings straight away, effective for new conversations only), launches a skill into chat, and reads how to add one with /add-skill; after the agent installs a skill in a conversation, a banner offers a fresh conversation that loads it.

22 mapped behaviors · 27 recipes and supporting checks · source snapshot 9 October 2026
From upstream main at 8793c111. Read the maintained source.

How to get to it

  • Sidebar Customize (sidebar-skills-link, goes to /customize, which redirects to /mcp on desktop), then Skills in the Customize navigation (sidebar-extensions-/skills).
  • Direct URL /skills, with optional filters: /skills?q=docker&state=enabled&recommendation=recommended&source=project&category=agent-authoring&type=knowledge.
  • Command menu (Control+k/Meta+k): searching "Skills" lists only Customize ("Browse skills, plugins, and integrations."), which lands on /mcp; then click Skills. There is no direct Skills command.
  • Phone: /customize shows the Customize hub (extensions-mobile-hub) with a Skills row; on /skills the header back button (sidebar-mobile-back-button) returns to the hub.
  • Install banner: inside a conversation, after the agent ran /add-skill <GitHub URL> (local backend only).
  • Project skills: a SKILL.md committed under <workspace>/.agents/skills/<name>/ in the folder picked with Open Workspace on Home (Local Repo or New Worktree mode); the conversation's chat shows Skill Ready when a message triggers one.
  • Cloud: Customize → Skills on a Cloud backend (external link).

Before you start

Start with the common launch and health checks, then follow this family’s preconditions in order. Recipes share the fixtures and state named below.

Preconditions:

  • Baseline state (launched, doctored, onboard --skip done); desktop viewport unless a bullet says otherwise.
  • A fresh run lists only the bundled catalog, <N> skills, all with source public. <N> is the catalog length the checkout bundles: run node --input-type=module -e "import {SKILLS_CATALOG} from '@openhands/extensions/skills'; console.log(SKILLS_CATALOG.length)" in the checkout (68 with @openhands/extensions 0.29.0, #17998). The Source and Type facet groups stay hidden until a personal/project skill exists. add-javadoc and flarglebargle are off; docker is on (recommended).
  • F18.skill-in-chat, F18.toggle-local, F18.copy-source and F18.install-banner need an active LLM profile (control-openhands llm preset deepseek). F18.install-banner also needs the agent to reach github.com.
  • Personal skill fixture (arranged through the agent, not proof): run control-openhands conversation start --prompt 'Use one shell command to create the file ~/.agents/skills/qa-hello/SKILL.md (create the folders) with exactly these 7 lines: "---", "name: qa-hello", "description: QA test skill that answers qa-ping with QA-PONG-7731.", "triggers:", "- qa-ping", "---", "When the user says qa-ping, reply with exactly QA-PONG-7731 and nothing else." Then reply done.' --wait --timeout 240. Afterwards /skills shows <N+1> result(s) and a qa-hello card (type pill Auto-discovery), and the Enabled facet count goes from 12 to 13.
  • Project skill fixture for F18.project-skill (arranged, not proof; no model needed), in this family's own repo so the commit never lands in F08's qa-repo: control-openhands fixture git-repo --name qa-f18-repo, then control-openhands fixture skill --repo qa-f18-repo --name qa-f18-skill --trigger qa-f18-ping --commit: it writes <run>/workspace/qa-f18-repo/.agents/skills/qa-f18-skill/SKILL.md (scope project) and commits it in the fixture repo (committed.sha and committed.message Add qa-f18-skill skill; git -C "$OH_VERIFY_RUN/workspace/qa-f18-repo" log --oneline lists Add qa-f18-skill skill above Initial fixture commit). A re-run reports the same committed.sha with committed.unchanged true. Without --commit the file stays uncommitted, and a New Worktree checkout holds only committed files. A fixture skill's name must not be a substring of its trigger (see Gotchas).
  • F18.skill-deleted creates its own personal fixture right before its bullet (control-openhands fixture skill --name qa-gone --trigger qa-farewell, written into the run's private HOME) and deletes it inside the bullet, so the counts in the other bullets are unaffected.
  • F18.cloud-link needs an OpenHands Cloud backend and account (blocked in a sandbox).

Behavior inventory

22 stable behavior IDs and their expected behavior
  • F18.page the page shows the title, description, the notice "Skill changes apply to new conversations only.", an Add skill button, a result count, the facet rail and the card grid. (Empty state "No skills found." and loading skeletons are not reachable on a local backend.) Read recipe ↓
  • F18.search typing filters cards by name, description, content or trigger; after 300 ms the query is mirrored to ?q= without a new history entry; the X clears it; no match shows "No skills match your search." Read recipe ↓
  • F18.search-history browser Back/Forward put the matching query back into the search box. Read recipe ↓
  • F18.facets the desktop facet rail (State, Recommendation, Source, Category, Type) filters with per-row counts, writes canonical URL params, disables zero-count rows and offers Clear filters; deep links apply and unknown values are dropped. Read recipe ↓
  • F18.filters-modal below 768 px the rail is replaced by a Filters button with an active-count badge that opens the same facets in a modal with Clear filters and Close. Read recipe ↓
  • F18.card each card shows icon, name, source, a two-line description and pills; extra pills collapse into a +N popover; click, Enter or Space opens the detail modal. Read recipe ↓
  • F18.copy-source personal and project skills (path sources) have a copy-path button on the card and in the modal that flips to "Copied to clipboard" for 2 s; built-in skills (source public) have none. Read recipe ↓
  • F18.toggle the card's plus/check toggle switches a built-in skill on or off; the choice survives a reload (enabled_skills allow-list on a local backend). Read recipe ↓
  • F18.toggle-local switching a personal/project skill off writes disabled_skills; new conversations then do not load it, and do again once it is back on. Read recipe ↓
  • F18.toggle-error a failed save shows an error toast and nothing is persisted. Read recipe ↓
  • F18.skill-in-chat an enabled skill is loaded into new conversations and fires on its trigger; a disabled one is not. Read recipe ↓
  • F18.project-skill a skill committed under <workspace>/.agents/skills/<name>/SKILL.md is loaded into every conversation started in that workspace, in Local Repo and New Worktree mode alike, and its trigger word activates it (the chat shows a Skill Ready row naming it); workspace skills are not listed on the Skills page. Read recipe ↓
  • F18.skill-deleted a personal skill whose folder was deleted disappears from the Skills page after a reload and is not loaded into new conversations: its trigger word activates nothing. Read recipe ↓
  • F18.personal-skill-dirs personal skills load from both user folders: ~/.agents/skills/<name>/SKILL.md and the persistence folder's skills/<name>/SKILL.md (~/.openhands/skills, which a Canvas stack moves to $OH_PERSISTENCE_DIR/skills). Each shows on the Skills page with its path and fires on its trigger in new conversations. Read recipe ↓
  • F18.detail-modal the detail modal shows source, an Enabled/Disabled switch, description, pills and the read-only content; switching off disables Use skill. Read recipe ↓
  • F18.detail-close the X, Close, Escape and a backdrop click close the detail modal. Read recipe ↓
  • F18.detail-pills the modal shows the full pill set (type, category, recommended, license, compatibility, version, triggers); pills that do not fit collapse into a +N button whose popover lists the rest without closing the modal. Read recipe ↓
  • F18.use-skill Use skill closes the modal, opens /conversations and pre-fills the composer with /<skill-name> . Read recipe ↓
  • F18.add-skill-modal Add skill opens an instructions-only modal with a copyable /add-skill example (which must point at an existing skill), steps, URL formats, storage notes and a docs link; Close, X and Escape close it. Read recipe ↓
  • F18.install-banner after the agent installs a skill with /add-skill, the conversation shows "Installed to this workspace: …" with Start new conversation with this skill (same workspace) and a dismiss X (session-only). Read recipe ↓
  • F18.phone at 390 px the page, cards and both modals fit without horizontal overflow; /customize shows a hub that links to Skills and the header back button returns to it. Read recipe ↓

Readable recipes

Read each script from top to bottom. Code is copied from the map; prose gives the action, expected observation, and conditions. <id>, <run> and similar placeholders stand for values from your own run. Short forms such as browser count continue the same control-openhands invocation; they are kept as documented.

Expected observations describe the recipe’s contract. Captures below selected recipes show representative real states from this snapshot; they do not mark every mapped behavior as passed. Follow cleanup before moving to another family.

Open from the sidebar #

  1. Note
    From / run
  2. Check
    control-openhands browser click 'testid=sidebar-skills-link' --expect-url '/mcp(\?|$)'
  3. Check
    control-openhands browser click 'testid=sidebar-extensions-/skills' --expect-url '/skills(\?|$)'
  4. Check
    control-openhands browser text 'testid=skills-settings-description'

    (Discover skills to add to your workspace. Search above the cards or filter by state, recommendation, and category, then open a card to see its details and use the skill. Enable or disable default skills. Disabled skills will not be loaded into agent context., describing this layout since #18035),

  5. Check
    control-openhands browser text 'testid=skills-new-conversation-notice'

    (Skill changes apply to new conversations only.) and

  6. Check
    control-openhands browser text 'testid=skills-result-summary'

    (<N> result(s) on a fresh run).

  7. Do
    control-openhands browser screenshot --feature F18.page --name desktop

    shows the Customize nav, the facet rail and the card grid.

Skills page with filter facets and a catalog of skill cards.
Browse the skills catalog with state, recommendation, source and category filters. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Sidebar Customize and Skills navigation opens the catalog with facets and result count.

Catalog browse only; no skill enablement or invocation asserted.

How this screenshot was taken

agent server: 1.53.0 · automation: 1.19.0 (launcher default) · canvas: 1.26.0

control-openhands browser goto /
control-openhands browser click testid=sidebar-skills-link --expect-url '/mcp(\?|$)'
control-openhands browser click testid=sidebar-extensions-/skills --expect-url '/skills(\?|$)'
control-openhands browser text testid=skills-result-summary
control-openhands browser screenshot --feature F18.page --name catalog

Command-menu entry #

  1. Note
    From / run
  2. Do
    control-openhands browser press Control+k
  3. Do
    control-openhands browser type 'testid=command-menu >> role=combobox' Skills
  4. Check
    control-openhands browser snapshot 'testid=command-menu'

    (one option, Customize Browse skills, plugins, and integrations. Go),

  5. Do
    control-openhands browser press Enter
  6. Wait
    control-openhands browser wait-url '/mcp(\?|$)'
  7. Check
    control-openhands browser click 'testid=sidebar-extensions-/skills' --expect-url '/skills(\?|$)'

Search #

  1. Note
    On /skills run
  2. Do
    control-openhands browser fill 'testid=skills-search-input' docker
  3. Check
    control-openhands browser text 'testid=skills-result-summary'

    (6 result(s) on a fresh run: content matches count too) and, a second later,

  4. Check
    control-openhands browser url

    (ends in /skills?q=docker);

  5. Do
    control-openhands browser eval "history.length"

    is the same before and after typing.

  6. Do
    control-openhands browser fill 'testid=skills-search-input' zzqa-nomatch
  7. Check
    control-openhands browser text 'testid=skills-no-match'
  8. Note
    : No skills match your search. with 0 result(s).
  9. Note
    Clear with
  10. Do
    control-openhands browser click 'testid=skills-toolbar >> role=button[name="Clear search"]'
  11. Check
    control-openhands browser value 'testid=skills-search-input'

    is empty, the full count is back and ?q= leaves the URL.

Back and forward #

  1. Do
    control-openhands browser goto '/skills?q=docker'
  2. Check
    control-openhands browser click 'testid=skill-facet-state-enabled'

    (pushes ?q=docker&state=enabled),

  3. Do
    control-openhands browser fill 'testid=skills-search-input' github
  4. Note
    wait a second, then
  5. Do
    control-openhands browser back
  6. Check
    control-openhands browser value 'testid=skills-search-input'
  7. Note
    : docker, URL /skills?q=docker.
  8. Do
    control-openhands browser forward
  9. Note
    gives github and /skills?q=github&state=enabled.

Facets #

  1. Note
    On /skills run
  2. Do
    control-openhands browser click 'testid=skill-facet-recommendation-recommended'

    (URL ?recommendation=recommended, 12 result(s)),

  3. Check
    control-openhands browser click 'testid=skill-facet-state-enabled'

    (URL ?state=enabled&recommendation=recommended: canonical order, not click order),

  4. Check
    control-openhands browser attr 'testid=skill-facet-state-enabled' aria-checked

    (true),

  5. Check
    control-openhands browser enabled 'testid=skill-facet-state-disabled'

    (false, its count reads 0),

  6. Do
    control-openhands browser click 'testid=skill-facet-category-agent-authoring'

    (8 result(s)), then

  7. Do
    control-openhands browser click 'testid=skills-clear-filters'
  8. Note
    : URL /skills, all results, and
  9. Check
    control-openhands browser count 'testid=skills-clear-filters'

    is 0.

Source and Type facets #

  1. Note
    With the qa-hello fixture,
  2. Check
    control-openhands browser text 'testid=skill-facet-group-source'

    lists This project 1 and Built in <N>, and testid=skill-facet-group-type lists Auto-discovery 1 and Trigger-based <N>.

  3. Do
    control-openhands browser click 'testid=skill-facet-source-project'
  4. Note
    gives ?source=project and 1 result(s); after Clear filters,
  5. Do
    control-openhands browser click 'testid=skill-facet-type-agentskills'
  6. Note
    gives ?type=agentskills and only qa-hello (control-openhands browser eval "[...document.querySelectorAll('[data-testid^=skill-name-]')].map(e=>e.textContent).join(',')").
  7. Note
    Clear filters again.

Deep link #

  1. Do
    control-openhands browser goto '/skills?category=bogus&type=knowledge&source=project'
  2. Note
    on a fresh run,
  3. Check
    control-openhands browser text 'testid=skills-result-summary'

    (0 result(s)) and

  4. Check
    control-openhands browser text 'testid=skill-facet-group-source'
  5. Note
    : SOURCE, This project 0, Built in <N>.
  6. Expect
    A URL selection keeps its group visible even when it matches nothing, and bogus is ignored.
  7. Note
    Clear with testid=skills-clear-filters.

Card and pill overflow #

  1. Check
    control-openhands browser testids 'testid=skill-card-add-javadoc'

    (name, skill-source-add-javadoc = public, skill-toggle-add-javadoc, description, pills, skill-triggers-add-javadoc-overflow labelled Show 2 more),

  2. Do
    control-openhands browser click 'testid=skill-triggers-add-javadoc-overflow'
  3. Check
    control-openhands browser text 'testid=skill-triggers-add-javadoc-overflow-popover'

    (java documentation, document java);

  4. Check
    control-openhands browser count 'testid=skill-detail-modal'
  5. Note
    stays 0.
  6. Do
    control-openhands browser screenshot --feature F18.card --name overflow-popover
  7. Do
    control-openhands browser press Escape

Keyboard open #

  1. Do
    control-openhands browser focus 'testid=skill-card-docker'
  2. Do
    control-openhands browser press Enter
  3. Check
    control-openhands browser attr 'testid=skill-detail-modal' data-skill-name

    (docker);

  4. Do
    control-openhands browser press Escape
  5. Note
    focus again,
  6. Do
    control-openhands browser press Space
  7. Check
    control-openhands browser count 'testid=skill-detail-modal'

    (1).

  8. Note
    Escape closes it.
Docker skill detail modal showing description, metadata and readable skill content.
Pressing Enter on a skill card opens its detail and usage information. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Docker detail modal opens from a focused card using Enter.

The skill is not invoked.

How this screenshot was taken

agent server: 1.53.0 · automation: 1.19.0 (launcher default) · canvas: 1.26.0

control-openhands browser goto /skills
control-openhands browser focus testid=skill-card-docker
control-openhands browser press Enter
control-openhands browser screenshot --feature F18.card --name docker-detail

Copy the source path #

  1. Note
    With the qa-hello fixture,
  2. Check
    control-openhands browser attr 'testid=skill-source-qa-hello' title

    is the full …/.agents/skills/qa-hello/SKILL.md path.

  3. Do
    control-openhands browser click 'testid=skill-copy-source-qa-hello'
  4. Check
    control-openhands browser attr 'testid=skill-copy-source-qa-hello' aria-label

    (Copied to clipboard; count 'testid=skill-detail-modal' stays 0),

  5. Check
    control-openhands browser clipboard

    (text is the same full SKILL.md path) and, after 2 s, the same attr (Copy source path).

  6. Note
    In the modal (control-openhands browser click 'testid=skill-card-qa-hello')
  7. Do
    control-openhands browser click 'testid=skill-modal-copy-source-qa-hello'
  8. Note
    turns its label to Copied to clipboard and the modal stays open. count 'testid=skill-copy-source-add-javadoc' is 0.

Toggle a built-in skill #

  1. Check
    control-openhands browser attr 'testid=skill-toggle-add-javadoc' aria-checked

    (false; aria-label is Enable skill),

  2. Do
    control-openhands browser click 'testid=skill-toggle-add-javadoc'

    (no modal opens; aria-checked turns true and the Enabled facet count goes up by one), wait a second,

  3. Do
    control-openhands browser reload
  4. Note
    and read aria-checked again: true.
  5. Check
    control-openhands api GET /api/settings --pick misc_settings.app_preferences.enabled_skills

    lists add-javadoc (--pick paths start inside body).

  6. Note
    To switch an enabled skill off from its card,
  7. Do
    control-openhands browser hover 'testid=skill-toggle-add-javadoc'
  8. Note
    first (attr ... data-showing-remove is true), then click (see Gotchas).

Toggle a personal skill #

  1. Do
    control-openhands browser hover 'testid=skill-toggle-qa-hello'
  2. Do
    control-openhands browser click 'testid=skill-toggle-qa-hello'
  3. Note
    wait a second,
  4. Do
    control-openhands browser reload
  5. Check
    control-openhands browser attr 'testid=skill-toggle-qa-hello' aria-checked

    (false).

  6. Check
    control-openhands api GET /api/settings

    shows disabled_skills: ["qa-hello"] under body.misc_settings.app_preferences and enabled_skills without it.

  7. Wait
    control-openhands conversation start --prompt "qa-ping" --wait --timeout 240
  8. Check
    control-openhands api GET '/api/conversations/<id>/events/search?limit=5' --max-bytes 2000000

    (id from conversation start): the user MessageEvent has "activated_skills": [].

  9. Note
    Back on /skills (control-openhands browser goto /skills; conversation start left the browser on the conversation), click the toggle on again (no hover needed), start another qa-ping conversation: "activated_skills": ["qa-hello"] and
  10. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'

    is QA-PONG-7731; disabled_skills is [] again.

Skill reaches chat #

  1. Do
    control-openhands browser goto /skills

    (the previous bullet ends on a conversation),

  2. Do
    control-openhands browser click 'testid=skill-toggle-flarglebargle'

    (aria-checked true), then

  3. Wait
    control-openhands conversation start --prompt "flarglebargle" --wait --timeout 240
  4. Check
    control-openhands conversation events <id> --kinds MessageEvent --from-start

    (the user row has skills: ["flarglebargle"]; the events/search?limit=5 read this bullet used before can miss the user row) and

  5. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'

    (a reply praising how smart the user is).

  6. Check
    control-openhands browser screenshot --feature F18.skill-in-chat --name enabled
  7. Note
    Switch it off again on /skills (control-openhands browser goto /skills, hover, click; enabled_skills no longer lists it) and start another conversation with --prompt "flarglebargle. Do not run any tools; reply in one line.": the user row has no skills, and the agent does not praise the user (2026-10-08: Flarglebargle to you too — what would you like help with?).

Project skill in a workspace #

  1. Note
    With the committed qa-f18-skill fixture, run
  2. Do
    control-openhands browser goto /skills
  3. Do
    control-openhands browser reload
  4. Check
    control-openhands browser count 'testid=skill-card-qa-f18-skill'
  5. Note
    : 0 (the catalog lists the Agent Server's own skills, not a workspace's).
  6. Wait
    control-openhands conversation start --workspace qa-f18-repo --prompt "qa-f18-ping" --wait --timeout 240

    (note <repo-skill-id>; its workspace is the fixture's path) and, in the chat it leaves open,

  7. Check
    control-openhands browser text 'testid=generic-event-message-title'

    (Skill Ready; with a model a second title, Invoked skill qa-f18-skill, can follow, because the agent may also call invoke_skill),

  8. Do
    control-openhands browser click 'role=button[name="Expand"]'
  9. Check
    control-openhands browser count 'role=button[name="qa-f18-skill"]'

    (1, under Triggered Skill Knowledge:);

  10. Do
    control-openhands browser screenshot --feature F18.project-skill --name skill-ready
  11. Note
    With an LLM the agent also obeys the skill, and this is the moment to read it, while the chat is still open:
  12. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'
  13. Note
    ends with QA_SKILL_OK; without a key the conversation ends in error right after the events below and that reply check is blocked (prerequisite: DEEPSEEK_API_KEY).
  14. Note
    Agent side,
  15. Check
    control-openhands conversation events <repo-skill-id> --grep qa-f18-skill --from-start
  16. Note
    matches the SystemPromptEvent (its <available_skills> lists <name>qa-f18-skill</name>) and the user MessageEvent (skills: ["qa-f18-skill"], with excerpts showing "activated_skills":["qa-f18-skill"] and the …/qa-f18-repo/.agents/skills/qa-f18-skill/SKILL.md path).
  17. Note
    New Worktree: run
  18. Do
    control-openhands workspace open qa-f18-repo --mode new_worktree

    (it picks the mode in the preview bar's selector and prints mode new_worktree and preview qa-f18-repo / New Worktree; control-openhands browser text 'testid=home-git-control-bar-preview' reads the same two lines), then

  19. Wait
    control-openhands conversation start --stay --prompt "qa-f18-ping" --wait --timeout 240

    (note <worktree-id>): its workspace is /tmp/conversation-worktrees/<worktree-id>/qa-f18-repo, a worktree of the fixture on branch openhands/<worktree-id> (read-only check: git -C /tmp/conversation-worktrees/<worktree-id>/qa-f18-repo log --oneline lists Add qa-f18-skill skill above Initial fixture commit),

  20. Check
    control-openhands browser text 'testid=generic-event-message-title'

    is Skill Ready again (and the same reply check applies here, with a key), and

  21. Check
    control-openhands conversation events <worktree-id> --grep qa-f18-skill --from-start
  22. Note
    matches the same events with the worktree's SKILL.md path (a ConversationStateUpdateEvent carrying the agent context matches in both conversations too).
  23. Note
    Restore the mode:
  24. Do
    control-openhands workspace open qa-f18-repo --mode local_repo

    (mode local_repo, preview qa-f18-repo / Local Repo).

Deleted personal skill #

  1. Arrange
    control-openhands fixture skill --name qa-gone --trigger qa-farewell
  2. Do
    control-openhands browser goto /skills
  3. Do
    control-openhands browser reload
  4. Check
    control-openhands browser count 'testid=skill-card-qa-gone'

    (1; control-openhands browser attr 'testid=skill-source-qa-gone' title is the run's …/private/home/.agents/skills/qa-gone/SKILL.md path and control-openhands browser text 'testid=skills-result-summary' counts one result more than before) and

  5. Do
    control-openhands browser screenshot --feature F18.skill-deleted --name present
  6. Note
    Prove it loads:
  7. Wait
    control-openhands conversation start --prompt "qa-farewell" --wait --timeout 240

    (note <gone-a>),

  8. Check
    control-openhands browser text 'testid=generic-event-message-title'

    (Skill Ready) and

  9. Check
    control-openhands conversation events <gone-a> --grep qa-gone --from-start
  10. Note
    : the SystemPromptEvent and the user MessageEvent match, the latter with skills: ["qa-gone"].
  11. Note
    Delete the folder (outside the UI; it is inside the run): rm -rf "$OH_VERIFY_RUN/private/home/.agents/skills/qa-gone".
  12. Do
    control-openhands browser goto /skills
  13. Do
    control-openhands browser reload
  14. Check
    control-openhands browser count 'testid=skill-card-qa-gone'

    (0; the summary is back to its earlier count),

  15. Wait
    control-openhands conversation start --prompt "qa-farewell. Do not run any tools; reply in one line." --wait --timeout 240

    (note <gone-b>; with a model, a bare qa-farewell sent deepseek-flash searching the disk for the word, and it quoted the run's stack.log line Skill 'qa-gone' triggered by keyword 'qa-farewell', so --grep qa-gone matched 9 tool rows although the skill was not loaded),

  16. Check
    control-openhands browser count 'text=Skill Ready'

    (0; control-openhands browser screenshot --feature F18.skill-deleted --name absent) and

  17. Check
    control-openhands conversation events <gone-b> --grep qa-gone --from-start
  18. Note
    : count is 0, and
  19. Check
    control-openhands conversation events <gone-b> --kinds MessageEvent --from-start

    shows the user message without a skills row.

  20. Expect
    No model is needed: the skill set is fixed before the first model call.

Both personal skill folders #

  1. Expect
    The qa-gone bullet above used ~/.agents/skills.
  2. Note
    For the other folder, write a fixture into the stack's persistence folder (outside the UI, inside the run; fixture skill writes only ~/.agents/skills): mkdir -p "$OH_VERIFY_RUN/private/skills/qa-legacy" and a SKILL.md there with the front matter name: qa-legacy, description: QA fixture skill in the persistence-dir skills folder., triggers: [qa-legacy-ping] and the body When this skill is active, end your reply with the word QA_LEGACY_OK. ($OH_VERIFY_RUN/private is the Agent Server's OH_PERSISTENCE_DIR, which replaces ~/.openhands).
  3. Do
    control-openhands browser goto /skills
  4. Do
    control-openhands browser reload
  5. Check
    control-openhands browser count 'testid=skill-card-qa-legacy'

    (1) and

  6. Check
    control-openhands browser attr 'testid=skill-source-qa-legacy' title

    (…/private/skills/qa-legacy/SKILL.md).

  7. Wait
    control-openhands conversation start --prompt "qa-legacy-ping. Do not run any tools; reply in one line." --wait --timeout 240

    (note <legacy-id>):

  8. Check
    control-openhands browser text 'testid=generic-event-message-title'

    is Skill Ready,

  9. Check
    control-openhands conversation events <legacy-id> --kinds MessageEvent --from-start

    shows the user row with skills: ["qa-legacy"], and with a model

  10. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'
  11. Note
    ends with QA_LEGACY_OK (control-openhands browser screenshot --feature F18.personal-skill-dirs --name persistence-dir-skill).
  12. Note
    Clean up with rm -rf "$OH_VERIFY_RUN/private/skills/qa-legacy", browser goto /skills, browser reload; the card count is 0.

Save failure #

  1. Do
    control-openhands browser goto /skills
  2. Note
    wait for testid=skill-toggle-add-javadoc and note its aria-checked (true when this follows the toggle bullet, which left add-javadoc on).
  3. Do
    control-openhands service stop agent-server
  4. Do
    control-openhands browser click 'testid=skill-toggle-add-javadoc' --timeout 5000

    (the card flips at once), wait about 2 s (the PATCH /api/settings is tried three times before the error shows; browser network lists three 502s) and

  5. Check
    control-openhands browser toasts --history
  6. Note
    : HTTP request failed (502 Bad Gateway): "Bad Gateway: connect ECONNREFUSED 127.0.0.1:<agent-server port>".
  7. Do
    control-openhands browser screenshot --feature F18.toggle-error --name toast
  8. Do
    control-openhands restart --timeout 240
  9. Do
    control-openhands browser goto /skills
  10. Note
    : aria-checked on testid=skill-toggle-add-javadoc is back to the value noted before the click and enabled_skills is unchanged.
  11. Note
    Clear the expected 502 noise with
  12. Check
    control-openhands browser errors --clear

Detail modal and switch #

  1. Note
    With add-javadoc on, run
  2. Do
    control-openhands browser click 'testid=skill-card-add-javadoc'
  3. Check
    control-openhands browser testids 'testid=skill-detail-modal'

    (name, source, skill-modal-enable-row-add-javadoc, description, pills incl. license MIT, skill-modal-field-content-add-javadoc, skill-detail-close, skill-detail-use-skill-add-javadoc) and

  4. Check
    control-openhands browser enabled 'testid=skill-detail-use-skill-add-javadoc'

    (true).

  5. Note
    Switch off with
  6. Do
    control-openhands browser click 'testid=skill-modal-enable-row-add-javadoc >> text=Enabled'
  7. Do
    control-openhands browser eval "document.querySelector('[data-testid=skill-modal-toggle-add-javadoc]').checked"

    is false, the row reads Disabled, enabled on Use skill is false and the card toggle behind reads aria-checked false.

  8. Do
    control-openhands browser screenshot --feature F18.detail-modal --name disabled
  9. Note
    Close, reload: the card stays off and enabled_skills no longer lists add-javadoc.

Close the modal #

  1. Note
    With testid=skill-card-docker opened each time, each of
  2. Do
    control-openhands browser click 'testid=skill-detail-modal-close'
  3. Do
    control-openhands browser click 'testid=skill-detail-close'
  4. Do
    control-openhands browser press Escape
  5. Do
    control-openhands browser mouse-click 100 500

    (backdrop) leaves

  6. Check
    control-openhands browser count 'testid=skill-detail-modal'
  7. Note
    at 0.

Pill overflow in the modal #

  1. Do
    control-openhands browser click 'testid=skill-card-add-javadoc'
  2. Check
    control-openhands browser text 'testid=skill-modal-pills-add-javadoc'

    (Trigger-based, Code quality & review, MIT, Requires Java source files, +3; license and compatibility appear only in the modal),

  3. Check
    control-openhands browser attr 'testid=skill-modal-pills-add-javadoc-overflow' aria-label

    (Show 3 more),

  4. Do
    control-openhands browser click 'testid=skill-modal-pills-add-javadoc-overflow'
  5. Check
    control-openhands browser text 'testid=skill-modal-pills-add-javadoc-overflow-popover'

    (javadoc, java documentation, document java); count 'testid=skill-detail-modal' stays 1.

  6. Do
    control-openhands browser screenshot --feature F18.detail-pills --name popover
  7. Note
    Close the popover with
  8. Do
    control-openhands browser click 'testid=skill-modal-description-add-javadoc'

    (popover count 0, modal still open), then

  9. Do
    control-openhands browser press Escape
  10. Note
    Expected as well: Escape with the popover open closes the popover (as it does on a card); at 0c446b8 it closes neither the popover nor the modal (see Gotchas).

Use skill #

  1. Do
    control-openhands browser click 'testid=skill-card-docker'
  2. Check
    control-openhands browser click 'testid=skill-detail-use-skill-docker' --expect-url '/conversations(\?|$)'
  3. Do
    control-openhands browser eval "document.querySelector('[data-testid=chat-input]').innerText"
  4. Note
    : /docker (the skill name as a slash command), with the modal gone (control-openhands browser count 'testid=skill-detail-modal' is 0) and the composer focused (control-openhands browser eval "document.activeElement.getAttribute('data-testid')" is chat-input).
  5. Do
    control-openhands browser screenshot --feature F18.use-skill --name prefilled

    shows /docker in the Home composer.

  6. Note
    Nothing is sent, and the text is applied once: leave and come back without a page load (control-openhands browser click 'testid=sidebar-automations-link' --expect-url '/automations$', then control-openhands browser click 'testid=sidebar-conversations-link' --expect-url '/conversations(\?|$)') and the same chat-input eval returns an empty string.
  7. Expect
    A browser goto / would prove nothing here: a page load empties the composer either way.

Add skill instructions #

  1. Do
    control-openhands browser goto /skills

    (Use skill ends on /conversations),

  2. Do
    control-openhands browser click 'testid=skills-add-skill-button'
  3. Check
    control-openhands browser text 'testid=add-skill-modal'

    (title Add a skill, the example /add-skill https://github.com/OpenHands/extensions/tree/main/skills/code-review, five steps, "Supported URL formats" with the short form OpenHands/extensions/skills/code-review, "Where skills are stored", the GITHUB_TOKEN note) and

  4. Check
    control-openhands browser attr 'testid=add-skill-modal-docs-link' href

    (https://docs.openhands.dev/overview/skills/adding#adding-new-skills; target is _blank).

  5. Note
    Copy with
  6. Do
    control-openhands browser click 'testid=add-skill-modal-example-copy'
  7. Note
    : its aria-label becomes Copied to clipboard, enabled is false and
  8. Check
    control-openhands browser clipboard

    returns the example command; after 2 s it reads Copy to clipboard again.

  9. Note
    Close with testid=add-skill-modal-dismiss, testid=add-skill-modal-close or
  10. Do
    control-openhands browser press Escape
  11. Note
    count 'testid=add-skill-modal' is 0 each time.
  12. Expect
    The example points at an existing skill (#18035):
  13. Do
    control-openhands browser goto '/skills?q=code-review'
  14. Check
    control-openhands browser text 'testid=skill-source-code-review'

    (public) show it in the bundled catalog, which F18.install-banner installs from that same URL.

Install banner #

  1. Wait
    control-openhands conversation start --prompt "/add-skill https://github.com/OpenHands/extensions/tree/main/skills/code-review" --wait --timeout 300
  2. Check
    control-openhands browser text 'testid=skill-install-restart-banner'
  3. Note
    : Installed to this workspace: code-review. Skills load when a conversation starts, so this conversation can't use them yet. plus Start new conversation with this skill.
  4. Do
    control-openhands browser screenshot --feature F18.install-banner --name banner
  5. Check
    control-openhands browser click 'testid=skill-install-restart-action' --expect-url '/conversations/(?!<id>)[0-9a-f-]+(\?|$)'

    (<id> from conversation start; the lookahead skips the current URL), then

  6. Check
    control-openhands conversation status <new id>

    (the id in the returned URL): its workspace equals the install conversation's workspace from conversation start.

  7. Note
    Back on the first conversation (control-openhands browser goto /conversations/<id>),
  8. Do
    control-openhands browser click 'testid=skill-install-restart-dismiss'
  9. Note
    makes count 'testid=skill-install-restart-banner' 0; after
  10. Do
    control-openhands browser reload
  11. Note
    it is 1 again (dismissal is session-only by design).

Filters modal on a phone #

  1. Do
    control-openhands browser viewport phone
  2. Do
    control-openhands browser goto /skills
  3. Check
    control-openhands browser visible 'testid=skill-facet-rail'

    (false),

  4. Do
    control-openhands browser click 'testid=skills-filters-button'
  5. Check
    control-openhands browser count 'testid=skill-filters-modal-clear'

    (0),

  6. Do
    control-openhands browser click 'testid=skill-filters-modal >> testid=skill-facet-state-enabled'

    (URL ?state=enabled, Clear filters appears),

  7. Do
    control-openhands browser screenshot --feature F18.filters-modal --name phone
  8. Do
    control-openhands browser click 'testid=skill-filters-modal-done'
  9. Check
    control-openhands browser text 'testid=skills-filters-button'

    (Filters and badge 1) and the summary (12 result(s) on a fresh run, 13 result(s) with the qa-hello fixture).

  10. Note
    Reopen and
  11. Do
    control-openhands browser click 'testid=skill-filters-modal-clear'
  12. Note
    : URL /skills and the Clear button disappears; Escape closes the modal.

Phone layout and hub #

  1. Note
    At the phone viewport run
  2. Check
    control-openhands browser bbox 'testid=skills-page'

    (pageHorizontalOverflow false),

  3. Do
    control-openhands browser screenshot --feature F18.phone --name list
  4. Note
    open testid=skill-card-docker and
  5. Check
    control-openhands browser bbox 'testid=skill-detail-modal'

    (insideViewport true), Escape,

  6. Do
    control-openhands browser click 'testid=skills-add-skill-button'
  7. Check
    control-openhands browser bbox 'testid=add-skill-modal'

    (insideViewport true), Escape.

  8. Do
    control-openhands browser goto /customize
  9. Check
    control-openhands browser text 'testid=extensions-mobile-hub'

    (Customize, MCP Servers, Skills, Plugins, Apps),

  10. Do
    control-openhands browser click 'testid=extensions-mobile-hub >> testid=sidebar-extensions-/skills' --expect-url '/skills(\?|$)'
  11. Check
    control-openhands browser click 'testid=sidebar-mobile-back-button' --expect-url '/customize(\?|$)'
  12. Note
    Return with
  13. Do
    control-openhands browser viewport desktop

Cloud link · Blocked prerequisite #

  1. Note
    Blocked: needs a Cloud backend.
  2. Note
    Expected:
  3. Check
    control-openhands browser attr 'testid=sidebar-extensions-/skills' href

    is <cloud host>/settings/skills with target _blank, and the Plugins and Apps items are hidden.

Restore #

  1. Note
    Leave add-javadoc and flarglebargle off and qa-hello on; the fixture skill and the conversations vanish with the run's private state.

Gotchas and known limits

  • sidebar-skills-link is labelled Customize and lands on /mcp on desktop (/customize redirects); Skills is one more click. Test ids embed the route: quote 'testid=sidebar-extensions-/skills'.
  • A browser click on an enabled card toggle is lost when the pointer was not already over it: pointerenter swaps the checkmark icon for the X icon between mousedown and mouseup, so Chrome fires no click and no PATCH /api/settings goes out (#17941). browser hover the toggle first (or click twice). Turning a skill on is not affected. Recorded as fail on F18.toggle.
  • The card toggle and the facet rows are role=switch/role=checkbox buttons: read aria-checked. The modal switch is a hidden <input> (#17900): click the row text and read .checked with browser eval.
  • Typing writes ?q= with replace after a 300 ms debounce, so read browser url a second later; facet clicks push a history entry immediately.
  • Facet counts are disjunctive: a row counts skills matching the other groups' selections, so Disabled reads 0 (and is disabled) while Recommended is checked.
  • Groups with fewer than two values are hidden (Source and Type on a fresh run) unless the URL selects one of their values.
  • The run's HOME lives under /tmp, so a skill in ~/.agents/skills is filed under Source This project (source=project), not Personal; with a real /home/<user> or /Users/<user> home it would be Personal.
  • conversation events rows show a user MessageEvent's activated skills as skills: [...] (since #18162). api GET '/api/conversations/<id>/events/search?limit=5' --max-bytes 2000000 shows the raw activated_skills, but only when the user row is among those five events. On 2026-10-08 it was not: the first five were the system prompt and state updates.
  • Skill lists are cached for 10 minutes: browser reload (or goto) after the agent creates a skill file, or the new card does not appear.
  • navigator.clipboard.readText() through browser eval hangs (no clipboard permission in the page); read what a copy button wrote with control-openhands browser clipboard and assert the button's aria-label too. The app runs on 127.0.0.1 (a secure context), so the unguarded clipboard.writeText calls cannot be tested for insecure-origin failures here.
  • With the Agent Server stopped, reloading /skills shows the "Manage backends / Disconnected" screen instead of the page: load the page first, then stop the service. After the failed save the card keeps showing the new state until a reload (#17941).
  • Before #18035 (closed #17939) the page description said "Search from the sidebar to filter the list" and offered curl and install flows the page does not have; a checkout without it still does.
  • With the modal's +N pill popover open (focus on its trigger), Escape closes neither the popover nor the detail modal; on a card the same Escape closes the popover (#17957). Cause not confirmed: the +N trigger's onKeyDown in src/components/features/skills/skill-card-pill-row.tsx stops propagation, which may keep the key from the popover's document and ModalBackdrop's window listeners. Click elsewhere in the modal first. Recorded as fail on F18.detail-pills.
  • The Add skill example, its Copy button and the add-skill Use skill message share one constant. Since #18035 it is .../skills/code-review; before, it pointed at skills/codereview, which is 404 upstream (the skill was renamed).
  • Skill changes apply to new conversations only; an open conversation keeps the skills it started with.
  • conversation events --grep searches whole events, the user's message text included: a trigger that contains the skill name (qa-gone-ping for qa-gone) matches the user MessageEvent even when the skill is not loaded. Give fixture skills a trigger that does not contain their name, or grep for <name>qa-gone</name>.
  • fixture skill --repo writes the project SKILL.md and records it in the repo only with --commit (committed.sha; a re-run reports unchanged); a New Worktree conversation checks out the repo's commits, so pass --commit. Workspace skills are loaded per conversation and never appear on /skills.
  • Without an LLM key the skill set is still observable: the SystemPromptEvent (available skills), the user MessageEvent (activated_skills) and the chat's Skill Ready row (generic-event-message-title; Expand lists the skills under Triggered Skill Knowledge:) all exist before the first model call, after which the conversation ends in error.

Source paths: src/routes/skills-settings.tsx, src/components/features/skills/, src/constants/skills-docs.ts, src/hooks/use-skill-enablement.ts, src/utils/skill-enablement.ts, src/hooks/use-launch-skill-in-chat.ts, src/api/skills-service.ts, src/components/features/chat/skill-install-restart-banner.tsx, src/utils/skill-install-events.ts.