Skip to main content

View source on GitHub

This example is a small command-line tool: you hand it a local agent-skills directory and it provisions a sandbox, copies your skills into the right place inside it, and starts a fresh conversation that can use them. It prints the URL of the conversation it just created. It builds on clone-and-attach, which prepares a sandbox (clone + setup.sh) and then attaches a conversation. Here we prepare the sandbox by uploading skills instead. Read start-sandbox first if the sandbox/agent-server split is new to you.

Why Would I Do This?

You have skills authored locally (a folder of SKILL.md files) and you want an agent to use them without committing them to a repo or installing them by hand. This script drops them into the sandbox’s user skills directory before the conversation starts, so the brand-new conversation loads them automatically.
Most of the time you don’t need this script. The standard ways to give a workspace skills are to store them in your repository as a plugin (e.g. under .openhands/skills/) and start the conversation with that plugin reference, or to run /add-skill <github-url> to add a skill to the workspace. Reach for this example when you want to push local, uncommitted skills straight into a sandbox programmatically — for automation, quick experiments, or skills you’re not ready to commit.

Where do the skills go, and why there?

When OpenHands starts a conversation it loads skills from several sources and merges them. User skills come from the sandbox user’s home:
  • ~/.openhands/skills/ ← this example’s default target
  • ~/.agents/skills/ ← newer AgentSkills location (use --remote-skills-dir)
  • ~/.openhands/microagents/ (legacy)
Because the upload happens before the conversation is created, the loader picks the skills up for that conversation. (The start-task lifecycle even has a dedicated SETTING_UP_SKILLS phase.) Putting them under the user home — rather than a repo’s .openhands/skills/ — means they load regardless of whether a repository is selected.
Already-running conversations are not re-scanned. Upload skills first, then start the conversation — which is exactly what this script does.

How It Works

Steps 1–2 and 5 use the Cloud app server (auth header X-Session-API-Key: <OH_API_KEY>). Steps 3–4 use the sandbox’s agent server (auth header X-Session-API-Key: <session_api_key>, returned by the create call). The agent server is where file upload and shell execution live — the Cloud app server has no file-upload route.

Recursive copy without per-file round-trips

Rather than upload files one at a time, the script tars the contents of your local directory (arcname=".") into a single .tar.gz, uploads that one archive, and extracts it with tar -xzf ... -C <target>. So a local layout like:
ends up as ~/.openhands/skills/code-review/SKILL.md and ~/.openhands/skills/deploy-helper/SKILL.md in the sandbox.

Run It

Sample output:
Open that URL and ask the agent to “say hello” — it should answer using the uploaded hello-openhands skill.

Point It at Your Own Skills

Every input is a flag with an environment-variable fallback, so the script is safe to drop into your own automation unchanged:

Reusing the sandbox so new conversations load these skills automatically

Once this script runs, the uploaded skills live in the sandbox’s home directory, so the sandbox is now a ready-made, skills-loaded environment. Whether new conversations reuse it — and therefore inherit those skills for free — is controlled by the Sandbox Grouping Strategy setting under Settings → Application.
  • Default — No Grouping (new sandbox per conversation): every new conversation gets its own fresh sandbox, so conversations you start later (in the UI or via the API) will not see the skills uploaded here.
  • Any grouping strategy (e.g. Group by Newest, Add to Any, Least Recently Used, Fewest Conversations): new conversations are added to an existing, still-running sandbox instead of a fresh one. Because every conversation reloads skills from the sandbox home when it starts, the conversations you create straight from the web UI land on this skills-loaded sandbox and pick up the uploaded skills automatically — no re-running this script needed.
So a typical flow is: run this script once to seed a sandbox with your skills, set a grouping strategy in the UI, then just open new conversations normally and they’ll already know your skills.
Caveat: this only works while the seeded sandbox is still running. A new conversation can only join a sandbox that is still running. If the seeded sandbox has gone inactive (paused after a stretch of no activity, but not yet deleted), a new conversation starts a fresh sandbox instead — without your skills. Reopening an existing conversation still brings its sandbox back (and your skills with it), and you can always reuse a specific sandbox — even an inactive one — by passing --sandbox-id <sandbox_id>, which wakes it up.
(If you’d rather stay scripted, you can also target a specific sandbox directly with --sandbox-id <sandbox_id> / SANDBOX_ID instead of relying on the grouping setting — this resumes the sandbox if it is paused.)

Skill format

Each skill is a directory containing a SKILL.md with YAML frontmatter:
Add an optional triggers: list to auto-activate a skill when keywords appear in a user message. See the bundled example-skills/ for a working sample.

Cleanup

The sandbox is intentionally left running because a live conversation is now attached to it — deleting the sandbox ends that conversation. Delete it from the conversation UI, or via the API (the id goes in both the path and a required sandbox_id query parameter):