Configure environment-specific overrides with profiles. A profile can add rules, hooks, MCP servers, and other settings that activate only when the profile is selected.
# Clean up from any previous run
rm -rf /tmp/ynh-tutorial
ynh uninstall local/profile-demo 2>/dev/null
mkdir -p /tmp/ynh-tutorialCreate a harness with a ci profile that adds stricter rules and a lint hook:
mkdir -p /tmp/ynh-tutorial/profile-harness/skills/deploy
mkdir -p /tmp/ynh-tutorial/profile-harness/rules
mkdir -p /tmp/ynh-tutorial/profile-harness/.ynh-plugin
cat > /tmp/ynh-tutorial/profile-harness/.ynh-plugin/plugin.json << 'EOF'
{
"$schema": "https://eyelock.github.io/ynh/schema/plugin.schema.json",
"name": "profile-demo",
"version": "0.1.0",
"default_vendor": "claude",
"hooks": {
"after_tool": [
{ "command": "/usr/local/bin/format-check.sh" }
]
},
"profiles": {
"ci": {
"hooks": {
"before_tool": [
{
"matcher": "Bash",
"command": "/usr/local/bin/ci-guard.sh"
}
]
},
"mcp_servers": {
"ci-db": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite", "/tmp/ci.db"]
}
}
},
"local": {
"mcp_servers": {
"dev-db": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite", "/tmp/dev.db"]
}
}
}
}
}
EOF
cat > /tmp/ynh-tutorial/profile-harness/instructions.md << 'EOF'
You are a deployment assistant. Follow safety procedures for all environments.
EOF
cat > /tmp/ynh-tutorial/profile-harness/skills/deploy/SKILL.md << 'EOF'
---
name: deploy
description: Deploy to staging or production
---
Run the deployment pipeline for the target environment.
EOF
cat > /tmp/ynh-tutorial/profile-harness/rules/safety.md << 'EOF'
Never deploy without running tests first.
EOFKey points:
profilesis a top-level field in.ynh-plugin/plugin.json- Each profile can contain
hooksandmcp_servers - Profiles declare only what they change — absent fields inherit from top-level defaults
- MCP servers are deep-merged (profile keys win on collision); hooks use per-event replace
- Set an MCP server to
nullin a profile to remove an inherited server
ynd validate /tmp/ynh-tutorial/profile-harnessExpected:
/tmp/ynh-tutorial/profile-harness: valid
The validator checks that profile names are valid and profile contents use the correct schema.
ynd preview /tmp/ynh-tutorial/profile-harness -v claude --profile ciExpected output includes:
.claude/hooks/hooks.jsonwith theciprofile'sbefore_toolhook and the inherited baseafter_toolhook (profiles use per-event merge — theciprofile'sbefore_toolreplaces the default, butafter_toolis inherited).claude/.mcp.jsonwith theci-dbMCP server from theciprofile (no base MCP servers to inherit)
Compare with the base (no profile):
ynd preview /tmp/ynh-tutorial/profile-harness -v claudeExpected: .claude/hooks/hooks.json has only the base after_tool hook. No .claude/.mcp.json (no MCP servers in base config).
Install the harness first:
ynh install /tmp/ynh-tutorial/profile-harnessLaunch interactively with the ci profile:
profile-demo --profile ciInside the Claude session, enable the plugin and reload to activate hooks and MCP servers:
/plugin enable profile-demo
/reload-plugins
Expected reload output includes: 3 hooks · 1 plugin MCP server (or similar counts). Then ask:
what hooks and MCP servers are configured?
The ci profile's before_tool hook replaces the base, and the ci-db MCP server is added. The base after_tool hook is inherited since the profile doesn't declare it.
Note: Claude Code's
--plugin-dirauto-activates skills and commands but not hooks or MCP servers. The/plugin enable+/reload-pluginsstep is needed to activate them. This is a Claude Code limitation — Codex and Cursor activate all plugin components automatically.
ynd preview /tmp/ynh-tutorial/profile-harness -v claude --profile nonexistentExpected error:
Error: profile "nonexistent" not defined in harness manifest
The YNH_PROFILE environment variable activates a profile without the flag:
YNH_PROFILE=ci ynd preview /tmp/ynh-tutorial/profile-harness -v claudeExpected: same output as --profile ci — the ci profile's settings are merged with the base values.
This is useful in CI/CD pipelines:
# .github/workflows/deploy.yml
env:
YNH_PROFILE: ci
steps:
- run: profile-demo -- "run deployment checks"YNH_PROFILE=local ynd preview /tmp/ynh-tutorial/profile-harness -v claude --profile ciExpected: the ci profile is active (not local). The --profile flag takes precedence over YNH_PROFILE.
Verify by checking the MCP output — you should see ci-db (from the ci profile), not dev-db (from the local profile).
Compare base vs profile output across vendors:
ynd diff /tmp/ynh-tutorial/profile-harness claude cursor --profile ciExpected output shows the vendor-specific differences with the ci profile applied to both:
- Claude: hooks in
.claude/hooks/hooks.json, MCP in.claude/.mcp.json - Cursor: hooks in
.cursor/hooks.json, MCP in.cursor/mcp.json
Compare with no profile to see what the profile adds:
ynd diff /tmp/ynh-tutorial/profile-harness claude cursorProfiles can also declare their own includes array. When the profile is active, those artifact sources are appended to the base harness's includes. This is how a single harness can carry a "user view" by default and a "contributor view" under a dev profile.
The include source can be a local filesystem path (local) — ideal for ship-alongside-the-manifest directories — or a remote Git URL (git), same as a top-level include.
Create a bundle directory next to the harness:
mkdir -p /tmp/ynh-tutorial/profile-harness/dev-extras/skills/deep-debug
cat > /tmp/ynh-tutorial/profile-harness/dev-extras/skills/deep-debug/SKILL.md << 'EOF'
---
name: deep-debug
description: Systematic debugging workflow for production incidents.
---
When invoked, walk through: reproduce, isolate, bisect, hypothesize, verify.
EOFAdd a profile that pulls it in:
cat > /tmp/ynh-tutorial/profile-harness/.ynh-plugin/plugin.json << 'EOF'
{
"$schema": "https://eyelock.github.io/ynh/schema/plugin.schema.json",
"name": "profile-demo",
"version": "0.1.0",
"default_vendor": "claude",
"profiles": {
"ci": {
"hooks": {
"before_tool": [
{
"matcher": "Bash",
"command": "/usr/local/bin/ci-guard.sh"
}
]
}
},
"dev": {
"includes": [
{"local": "dev-extras"}
]
}
}
}
EOFPreview without profile — only the base artifacts appear:
ynd preview /tmp/ynh-tutorial/profile-harness -v claude | grep SKILL
# Expected: only skills declared at the harness rootPreview with --profile dev — deep-debug appears on top of the base set:
ynd preview /tmp/ynh-tutorial/profile-harness -v claude --profile dev | grep deep-debug
# Expected: skills/deep-debug/SKILL.md shows upPaths in local are relative to the harness root (or absolute). The pick field filters which artifacts to include from the source. A profile cannot remove base includes — it only appends.
Profiles can be authored by hand-editing .ynh-plugin/plugin.json — that is what every step above did. They can also be edited from the command line, which is what an interactive consumer like TermQ uses. The CLI mirrors ynh include and routes through the same resolver, so edits land in the source tree for pointer-form local installs.
# Add a new profile (empty) and a hook inside it
ynh profile add /tmp/ynh-tutorial/profile-harness staging
ynh profile hook add /tmp/ynh-tutorial/profile-harness staging before_tool "echo staging guard"
# Add an MCP server to the profile
ynh profile mcp add /tmp/ynh-tutorial/profile-harness staging \
notes --command npx --arg -y --arg @modelcontextprotocol/server-memory
# Verify it landed in the manifest
ynd compose /tmp/ynh-tutorial/profile-harness --profile staging --format json | grep staging
# Expected: profiles.staging present with the new hook and mcp_server
# Update the MCP server
ynh profile mcp update /tmp/ynh-tutorial/profile-harness staging \
notes --arg -y --arg @modelcontextprotocol/server-memory --arg --verbose
# Remove individual entries
ynh profile hook remove /tmp/ynh-tutorial/profile-harness staging before_tool 0
ynh profile mcp remove /tmp/ynh-tutorial/profile-harness staging notes
# Remove the profile itself (refused if any focus still references it)
ynh profile remove /tmp/ynh-tutorial/profile-harness stagingThe first positional argument is either a filesystem path (as above, while you are authoring) or a canonical harness id (local/profile-demo, github.com/<org>/<repo>/<name>) once the harness is installed.
See reference.md for the full set of ynh profile flags and profiles.md §"CLI Editing" for the surrounding surface.
ynh uninstall local/profile-demo 2>/dev/null
rm -rf /tmp/ynh-tutorial- Profiles are declared in
.ynh-plugin/plugin.jsonunderprofilesas named config objects - Each profile can override
hooks,mcp_servers, and addincludes - Profiles use merge semantics: MCP servers are deep-merged (profile keys win), hooks use per-event replace (absent events inherited), includes are appended (profile cannot remove a base include)
- Set an MCP server to
nullin a profile to remove an inherited server --profile <name>activates a profile onynh run,ynd preview, andynd diffYNH_PROFILEenv var activates a profile (flag takes precedence)- Invalid profile names produce helpful errors listing available profiles
- Profile-level
includessupport both remote (git) and local (local) sources — same shape as top-level includes ynd validatechecks profile schema validity- Profiles can be edited with
ynh profile add/remove,ynh profile hook add/remove,ynh profile mcp add/update/remove, andynh profile include add/remove/update— the same surface a GUI consumer drives
Tutorial 7: Focus — bind a prompt and profile for repeatable, non-interactive runs.