Skip to content

feat: ship as a Claude Code plugin (and Codex marketplace)#13

Merged
borski merged 7 commits into
mainfrom
feat/claude-plugin
May 1, 2026
Merged

feat: ship as a Claude Code plugin (and Codex marketplace)#13
borski merged 7 commits into
mainfrom
feat/claude-plugin

Conversation

@borski
Copy link
Copy Markdown
Owner

@borski borski commented May 1, 2026

Summary

Adds a Claude Code plugin packaging on top of the existing repo. Users can install via /plugin marketplace add borski/travel-hacking-toolkit instead of cloning. Cuts the launch command from claude --strict-mcp-config --mcp-config .mcp.json to just claude.

The Codex marketplace already shipped before this PR; this PR doesn't change it. Both marketplaces coexist in the repo. The new Claude plugin sits at .claude-plugin/.

What users will run after this merges

Claude Code (new):

/plugin marketplace add borski/travel-hacking-toolkit
/plugin install travel-hacker@borski-travel

Then run the local key setup script (or /travel-hacker:getting-started which points at it).

Codex (already worked, unchanged):

codex plugin marketplace add borski/travel-hacking-toolkit

Cowork (Claude Desktop's agent panel): install via Claude Code first; Cowork picks it up automatically because both share ~/.claude/plugins/.

OpenCode users still clone and run ./scripts/setup.sh. Unchanged.

API key setup: now via local script, not chat

Both reviewers (fresh Claude opus + fresh Codex) flagged that an earlier version of this PR walked the user through key entry inside the chat. That was unsafe: pasted keys would land in terminal scrollback, Claude Code session logs, and Anthropic API logs. Three logging surfaces.

The current setup ships two scripts: scripts/setup-keys.sh (POSIX, handles zsh/bash/fish) and scripts/setup-keys.ps1 (PowerShell + cmd guidance). They prompt with masked input, validate, write to the user's shell rc with a backup, and never echo values. The /travel-hacker:getting-started skill points at them and explicitly refuses to collect keys via chat.

Why we don't use plugin userConfig

The plugin spec includes userConfig for prompting/storing API keys at install. It's broken upstream for bash-based skills (anthropics/claude-code#11927 for Claude Code, #39125 for Cowork). Verified empirically that values stored in pluginConfigs.options never reach the agent's bash tool. When that's fixed upstream we can revisit.

Commits (13)

  1. feat(plugin): add Claude Code plugin manifest and marketplace
  2. feat(plugin): add travel-hacker agent definition
  3. test(plugin): add plugin validation to smoke test, simplify launch hints
  4. ci: prevent agent file from drifting from CLAUDE.md
  5. docs(plugin): document no-clone install for Claude Code and Codex
  6. feat(plugin): UX polish - userConfig prompts, guided skills, sample prompts (later reverted)
  7. feat(agent): graceful degradation when API keys are missing
  8. docs(plugin): correct how Codex resolves the install
  9. docs(plugin): document Cowork install via Claude Code CLI
  10. fix(plugin): drop userConfig, document shell env vars instead
  11. feat(plugin): expand getting-started into full setup flow (later replaced by setup-keys script)
  12. fix(plugin): never echo API key values in getting-started flow (interim hardening)
  13. fix(plugin): address review feedback from Claude + Codex code review ← the big consolidation

Tests

bash scripts/smoke-test.sh passes 16/16:

  • 10 static checks (manifest validation, agent sync + frontmatter, component presence, skill frontmatter, data freshness, table generation, etc.)
  • 3 agents x 2 checks each (codex, claude with --plugin-dir, opencode)

CI (GitHub Actions) runs the static checks on every PR push.

Manually verified end-to-end:

  • claude plugin install travel-hacker@borski-travel works from a local marketplace
  • codex plugin marketplace add borski/travel-hacking-toolkit continues to work (uses the pre-existing Codex marketplace)
  • 42 skills auto-discover as /travel-hacker:<name>
  • Skiplagged + Kiwi return real data on a SFO→LHR cash flight search with no API keys set
  • Agent surfaces missing keys once at the bottom: "Set Duffel/Ignav for a fuller picture"
  • The travel-hacker subagent loads with its full system prompt
  • Cowork picks up the plugin automatically after CLI install
  • scripts/install-hooks.sh refuses to overwrite a foreign pre-commit hook (verified end-to-end)
  • scripts/setup-keys.sh runs interactively with masked input, writes to ~/.zshrc with a backup, never echoes values

Plugin format separation

Format Manifest Marketplace Notes
Codex plugins/travel-hacking-toolkit/.codex-plugin/plugin.json .agents/plugins/marketplace.json Pre-existing
Claude .claude-plugin/plugin.json (at repo root) .claude-plugin/marketplace.json New in this PR

Anthropic's docs say Codex CAN read Claude-style marketplaces, but in practice when both exist Codex picks its own format. Both end users hit the same one-line install command.

Skill count

40 → 42. New: plan-trip, getting-started. README and llms.txt regenerated via gen-skill-tables.sh.

borski added 7 commits May 1, 2026 14:23
Adds the .claude-plugin/ directory with two files:

- plugin.json: plugin manifest with name, version, description, author,
  repo, license, keywords. Defines the 'travel-hacker' plugin.
- marketplace.json: marketplace catalog with one entry pointing at the
  repo root as the plugin source. Marketplace name 'borski-travel'.

Codex also reads .claude-plugin/marketplace.json (per Anthropic docs), so
the same file serves both ecosystems.

Validates clean via 'claude plugin validate .'.

Users install via:
  /plugin marketplace add borski/travel-hacking-toolkit
  /plugin install travel-hacker@borski-travel
The Claude plugin format expects an agents/ directory with .md files
that define subagents. CLAUDE.md is the existing source of truth for
the toolkit's system prompt (and for OpenCode/Codex via the AGENTS.md
symlink), so we add YAML frontmatter (name/description/model: opus) to
CLAUDE.md and copy it to agents/travel-hacker.md.

Symlinks and ${file} include syntax aren't honored in plugin agent
files, so it has to be a real file. To prevent drift between the two:

1. scripts/sync-agent.sh copies CLAUDE.md -> agents/travel-hacker.md.
   Validates that CLAUDE.md starts with --- frontmatter.

2. scripts/hooks/pre-commit auto-syncs when CLAUDE.md is staged. The
   hook is tagged with a sentinel comment so install-hooks.sh can tell
   ours apart from a contributor's existing pre-commit hook.

3. scripts/install-hooks.sh copies hooks from scripts/hooks/ to
   .git/hooks/. Refuses to overwrite a foreign (untagged) hook with a
   clear skip message. Setup.sh runs it automatically on clone.

4. .github/workflows/smoke-test.yml runs scripts/smoke-test.sh --quick
   on every PR. The smoke test fails if agents/travel-hacker.md drifts
   from CLAUDE.md. This is the real safety net.

Layered defense: contributors who skip setup don't get the hook, but CI
catches them anyway.
Two user-invoked orchestration skills (disable-model-invocation: true)
that surface as slash commands inside Claude Code:

/travel-hacker:plan-trip — guided trip planner. The hero command. Asks
for destination, dates, who, class, flexibility, points-or-cash. Then
runs the full hero workflow: lessons-learned + flight-search-strategy +
parallel cash search + parallel award search across all programs +
cross-reference with transfer-partners + cpp math + sweet-spots.
Output is a ranked markdown table with the winning option bolded and
booking next steps via booking-guidance.

/travel-hacker:getting-started — first-run setup detector and signpost.
Detects which API keys are configured. Tells the user what they CAN do
right now. Refuses to collect API keys via chat (keys typed into chat
get retained in three logging surfaces: terminal scrollback, Claude
Code session log, Anthropic API logs). Instead points users at
scripts/setup-keys.sh (or .ps1 for PowerShell) for safe local setup.
Shows sample prompts scaled to what they configured.

Both skills end with declarative 'Next steps:' statements rather than
action-offer questions, per the system prompt's PRE-OUTPUT GATE.
Anthropic's plugin userConfig is broken upstream for bash-based skills
(anthropics/claude-code#11927, #39125), so plugin users have to set
API keys as shell environment variables. We ship two scripts that do
this safely:

- scripts/setup-keys.sh (bash, handles zsh/bash/fish on macOS/Linux/WSL)
- scripts/setup-keys.ps1 (PowerShell, with cmd setx guidance for cmd users)

Design goals:

1. Keys never go through chat. The script runs in the user's terminal,
   prompts with masked input (read -s in bash, AsSecureString in
   PowerShell). Values never echo to stdout.

2. Shell-safe quoting. Exports are single-quoted (export KEY='value' or
   set -gx KEY 'value' or $env:KEY = 'value') so other metacharacters
   in the API key are inert. Single quotes themselves are explicitly
   rejected with a clear message.

3. Per-key minimum length validation. Default 10 chars. AwardWallet user
   IDs (numeric, 5-7 digits) and ENTUR_CLIENT_NAME (user-chosen short
   identifier) get a 3-char minimum. Prevents typos but doesn't reject
   legitimate short values.

4. Idempotent. Skips keys already exported in the rc file (per-shell
   regex). Never duplicates entries on repeated runs.

5. Backups before write. If the rc file already has content, copies it
   to <rcfile>.bak.YYYYMMDD-HHMMSS first.

6. Auto-creates missing rc files (zsh on a fresh macOS install,
   PowerShell $PROFILE on a fresh Windows box).

7. Persist-transient-key flow. If a key is set in the current shell
   but missing from the rc file, the script offers to persist it.
   Cautious default = no. Same validation applies (single-quote
   reject + min_len).

8. Tier-based prompting. Tier 1 high-value keys first (Seats.aero,
   Duffel, Ignav, AwardWallet). Tier 2 (extra sources) and Tier 3
   (Scandinavia transit) prompt only if the user opts in.

The /travel-hacker:getting-started skill points at these scripts and
explicitly refuses to collect keys via chat.
Adds five new static checks to scripts/smoke-test.sh:

1. setup-keys.sh syntax (bash -n). Catches the curl|bash one-liner
   breaking silently if a future edit introduces a syntax error.

2. setup-keys.ps1 structure (brace-counting). Same idea for the
   PowerShell version since we don't have pwsh in the local CI image.

3. Claude plugin manifest + marketplace validation via 'claude plugin
   validate .'. Skips cleanly if the claude CLI isn't installed.

4. agents/travel-hacker.md is in sync with CLAUDE.md (diff -q) AND has
   the required frontmatter fields (name, description, model). Guards
   against contributors editing CLAUDE.md without running sync-agent.sh,
   and against frontmatter regressions.

5. Plugin component discovery: skills/ non-empty + .mcp.json valid JSON.
   Catches the case where a directory was deleted or a config corrupted.

Also refactors the agent invocation test for the Claude path. Was
calling claude --strict-mcp-config --mcp-config .mcp.json -p; now uses
claude --plugin-dir 'REPO_DIR' -p. The plugin loads skills + MCPs +
agent system prompt atomically so the old MCP-only flag is obsolete.
The test_agent function is refactored to take an explicit binary
parameter so 'claude' and 'claude-plugin' aren't conflated.

Docker image manifest check now distinguishes network/auth errors from
genuinely missing images. If GHCR is unreachable from the runtime
environment (DNS, network, credentials), the test skips with a clear
message. Restricted CI environments no longer false-fail this check.

Updated header comment to enumerate the full check set (12 static + 3
agent invocations x 2 each = 18 total when all CLIs are installed).
Five changes to setup.sh, setup.ps1, and .gitignore, plus a deletion:

1. Drop the .claude/settings.local.json copy step. Anthropic's plugin
   userConfig was supposed to handle Claude Code key management but is
   broken upstream (anthropics/claude-code#11927). The settings.local.json
   path was the workaround pre-PR; now the canonical path is shell env
   vars set via scripts/setup-keys.sh. The example file is removed too.

2. Tell Claude users where to actually configure keys: shell rc + the
   guided /travel-hacker:getting-started skill. Replaces the obsolete
   'edit .claude/settings.local.json' instruction.

3. Show install-hooks.sh output instead of redirecting to /dev/null.
   When the script skips a foreign pre-commit hook, the contributor
   sees the message instead of silently losing sync enforcement.

4. Recommend 'claude --plugin-dir .' instead of the old
   '--strict-mcp-config --mcp-config .mcp.json' as the launch command
   from inside a clone. The plugin loads skills + MCPs + agent system
   prompt atomically, so the old MCP-only flag pair is obsolete.

5. .gitignore: also ignore .env.backup* and .env.bak so timestamped
   backups from local key rotations don't accidentally get staged.
…t tree

README quick-start rewritten around the no-clone install paths:

- Claude Code: /plugin marketplace add + /plugin install. Two commands.
  After install, run /travel-hacker:getting-started for guided key setup.
- Codex: codex plugin marketplace add. One command. Same marketplace
  catalog, different plugin format inside the repo (.codex-plugin vs
  .claude-plugin).
- Cowork (Claude Desktop's agent panel): no /plugin slash commands of
  its own, but inherits anything installed via Claude Code from
  ~/.claude/plugins/. Documents the indirection clearly.
- Clone path preserved for OpenCode users (no plugin format) and for
  contributors. claude --plugin-dir . loads skills + MCPs + agent
  atomically.

API key setup section now leads with scripts/setup-keys.sh and .ps1.
Manual setup with shell rc edits is preserved as a sub-heading.
Includes a 1Password 'op run --env-file=.env -- claude' option.
Notes the upstream userConfig bugs (anthropics/claude-code#11927,
#39125) so readers understand why the toolkit reads from shell env
vars instead of plugin settings.

First-run verification: /travel-hacker:getting-started or shell-side
'claude plugin list | grep travel-hacker'.

Project structure tree updated: adds .claude-plugin/, agents/,
.github/workflows/, scripts/setup-keys.{sh,ps1}, scripts/sync-agent.sh,
scripts/install-hooks.sh, scripts/hooks/pre-commit, plus the two new
orchestration skills (getting-started, plan-trip).

Smoke test description ('What it verifies') now lists all 12 static
checks instead of the obsolete 8.

Pre-existing miscount fixed: 'Six Docker-based skills' is actually
five skills plus a shared base image. Both occurrences in the README
updated.

Description tweaks: '6 MCP servers (5 free + LiteAPI which needs a
key)' clarifies what 'free' meant in the next paragraph. README claim
that the script 'rejects values with shell metacharacters' was an
overstatement; reworded to match what the validation actually does
(reject single quotes + min length check, plus single-quoted exports
defuse other metacharacters).

llms.txt regenerated via gen-skill-tables.sh to match.
@borski borski force-pushed the feat/claude-plugin branch from 3631c47 to 524820d Compare May 1, 2026 22:01
@borski borski merged commit 2c7e9e2 into main May 1, 2026
1 check passed
borski added a commit that referenced this pull request May 1, 2026
…verified

The previous wording overstated what the cited issues prove. #11927 is
about settings.json env blocks, #39125 is cowork-specific. Neither
directly proves plugin userConfig is broken for bash-tool skills. The
actual basis for the workaround is empirical testing during PR #13
build, not those upstream issues. Drop the implicit promise to migrate
once they're fixed since they may not be the right tracker for our case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant