# Adding a New Host to gstack gstack uses a declarative host config system. Each supported AI coding agent (Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes, GBrain) is defined as a typed TypeScript config object built by the `defineHost()` factory. Adding a new host means creating one file and re-exporting it. Zero code changes to the generator, setup, or tooling. ## How it works ``` hosts/ ├── define-host.ts # defineHost() factory: shared defaults + derived fields ├── claude.ts # Primary host ├── codex.ts # OpenAI Codex CLI ├── factory.ts # Factory Droid ├── kiro.ts # Amazon Kiro ├── opencode.ts # OpenCode ├── slate.ts # Slate (Random Labs) ├── cursor.ts # Cursor ├── openclaw.ts # OpenClaw ├── hermes.ts # Hermes (Nous Research) ├── gbrain.ts # GBrain └── index.ts # Registry: imports all, derives Host type ``` Each config file calls `defineHost()` and exports the resulting `HostConfig` object, which tells the generator: - Where to put generated skills (paths) - How to transform frontmatter (allowlist/denylist fields) - What Claude-specific references to rewrite (paths, tool names) - What binary to detect for auto-install - What resolver sections to suppress - What assets to symlink at install time The generator, setup script, platform-detect, uninstall, health checks, worktree copy, and tests all read from these configs. None of them have per-host code. ## Step-by-step: add a new host ### 1. Create the config file Configs are built with the `defineHost()` factory in `hosts/define-host.ts`. You only write the fields that differ from the common external-host defaults; everything else is derived from the host name. A fully-default host is two fields (see `hosts/slate.ts` or `hosts/cursor.ts`): ```typescript import { defineHost } from './define-host'; const myhost = defineHost({ name: 'myhost', displayName: 'MyHost', }); export default myhost; ``` That expands to the full `HostConfig` with these defaults: - `cliCommand: 'myhost'` (the name; binary for `command -v` detection) - `cliAliases: []` - `globalRoot` / `localSkillRoot`: `.myhost/skills/gstack`, `hostSubdir`: `.myhost` - `usesEnvVars: true` (false only for Claude, which uses literal `~` paths) - `frontmatter`: allowlist keeping `name` + `description`, no description limit - `generation`: no metadata file, `skipSkills: ['codex']` (codex skill is Claude-only) - `pathRewrites`: the standard trio derived from the resolved paths (`~/.claude/skills/gstack` → `~/{globalRoot}`, `.claude/skills/gstack` → `{localSkillRoot}`, `.claude/skills` → `{hostSubdir}/skills`) - `suppressedResolvers`: the GBrain pair (`GBRAIN_CONTEXT_LOAD`, `GBRAIN_SAVE_RESULTS`) - `runtimeRoot`: the shared asset list (`bin`, `browse/dist`, `browse/bin`, `gstack-upgrade`, `ETHOS.md` + review checklist files) - `install`: `{ linkingStrategy: 'symlink-generated' }` - `learningsMode: 'basic'` Override any field by passing it to `defineHost()`. Two path-rewrite options: - `extraPathRewrites`: appends entries AFTER the derived trio (e.g. kiro's codex-path cleanup, or `{ from: 'CLAUDE.md', to: 'AGENTS.md' }` for AGENTS.md hosts). Use this when the standard trio is right but you need more. - `pathRewrites`: replaces the derived list entirely. Only for non-mechanical cases — codex and factory rewrite the global path to `$GSTACK_ROOT` and add an extra review-path rewrite; claude has an empty list. The two are mutually exclusive (the factory throws if you pass both). Shared constants exported from `define-host.ts` for spread-composition: `CROSS_MODEL_RESOLVERS` (the five Codex-invoking resolvers suppressed on hosts that can't invoke other models), `GBRAIN_RESOLVERS` (the default suppression pair), and `EXEC_STYLE_TOOL_REWRITES` (the OpenClaw-style lowercase-tool rewrites shared by openclaw and gbrain). Good examples: `hosts/opencode.ts` (path + runtimeRoot overrides), `hosts/factory.ts` (tool rewrites and conditional fields), `hosts/hermes.ts` (AGENTS.md host with custom tool rewrites and resolver composition). ### 2. Register in the index Edit `hosts/index.ts`: ```typescript import myhost from './myhost'; // Add to ALL_HOST_CONFIGS array: export const ALL_HOST_CONFIGS: HostConfig[] = [ claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, myhost ]; // Add to re-exports: export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, myhost }; ``` ### 3. Add to .gitignore Add `.myhost/` to `.gitignore` (generated skill docs are gitignored). ### 4. Generate and verify ```bash # Generate skill docs for the new host bun run gen:skill-docs --host myhost # Verify output exists and has no .claude/skills leakage ls .myhost/skills/gstack-*/SKILL.md grep -r ".claude/skills" .myhost/skills/ | head -5 # (should be empty) # Generate for all hosts (includes the new one) bun run gen:skill-docs --host all # Health dashboard shows the new host bun run skill:check ``` ### 5. Run tests ```bash bun test test/gen-skill-docs.test.ts bun test test/host-config.test.ts ``` The parameterized smoke tests automatically pick up the new host. Zero test code to write. They verify: output exists, no path leakage, valid frontmatter, freshness check passes, codex skill excluded. ### 6. Update README.md Add install instructions for the new host in the appropriate section. ## Config field reference See `scripts/host-config.ts` for the full `HostConfig` interface with JSDoc comments on every field. Key fields: | Field | Purpose | |-------|---------| | `frontmatter.mode` | `allowlist` (keep only listed) or `denylist` (strip listed) | | `frontmatter.descriptionLimit` | Max chars, `null` for no limit | | `frontmatter.descriptionLimitBehavior` | `error` (fail build), `truncate`, `warn` | | `frontmatter.conditionalFields` | Add fields based on template values (e.g., sensitive → disable-model-invocation) | | `frontmatter.renameFields` | Rename template fields (e.g., voice-triggers → triggers) | | `pathRewrites` | Literal replaceAll on content. Order matters. Replaces the derived trio. | | `extraPathRewrites` | (defineHost input only) Appended after the derived trio. | | `toolRewrites` | Rewrite Claude tool names (e.g., "use the Bash tool" → "run this command") | | `suppressedResolvers` | Resolver functions that return empty for this host | | `coAuthorTrailer` | Git co-author string for commits | | `boundaryInstruction` | Anti-prompt-injection warning for cross-model invocations | ## Validation The `validateHostConfig()` function in `scripts/host-config.ts` checks: - Name: lowercase alphanumeric with hyphens - CLI command: alphanumeric with hyphens/underscores - Paths: safe characters only (alphanumeric, `.`, `/`, `$`, `{}`, `~`, `-`, `_`) - No duplicate names, hostSubdirs, or globalRoots across configs Run `bun run scripts/host-config-export.ts validate` to check all configs.