This file provides guidance to AI coding assistants β including Claude Code, GitHub Copilot, and similar tools β when working with code in this repository.
Hoist-react is the client-side component of the Hoist web application development toolkit, built
by Extremely Heavy Industries (xh.io). It is a library package (not a standalone app) published
as @xh/hoist and consumed by Hoist application projects. The server-side counterpart is
hoist-core.
- Language: TypeScript
- Framework: React with MobX for reactive state management
- Package manager: pnpm (version pinned via the
packageManagerfield inpackage.json)
IMPORTANT: Do not guess at hoist-react APIs, component props, or framework patterns. Hoist-react ships dedicated tools that provide structured access to all framework documentation and TypeScript type information. You MUST use these tools before modifying or extending hoist-react code to understand existing architecture, configuration patterns, and common pitfalls. The package READMEs and concept docs are the authoritative reference for how Hoist works -- skipping them risks producing code that conflicts with established patterns or misses built-in functionality.
Two interfaces are available. Both share the same underlying registries and produce identical output:
MCP Server (hoist-react) -- When working in the hoist-react repository, an MCP server is
configured via .mcp.json and is very likely already available. Use the hoist-search-docs,
hoist-list-docs, hoist-read-doc, hoist-search-symbols, hoist-get-symbol, and
hoist-get-members tools, plus hoist://docs/{id} resources for direct document access.
CLI Tools -- For environments without MCP support, or when you prefer shell commands. These are
real bin entries in the hoist-react package.json β invoke them exactly as shown with npx:
# Documentation
npx hoist-docs search "grid sorting" # Search all docs - returns ranked sections
npx hoist-docs read cmp/grid # Read a specific doc by ID
npx hoist-docs read cmp/grid --outline # List a doc's sections with token counts
npx hoist-docs read cmp/grid -s "Sorting" # Read one section (cheaper than the whole doc)
npx hoist-docs list # List all available docs
npx hoist-docs conventions # Print coding conventions
npx hoist-docs index # Print the documentation catalog
# TypeScript symbols and types
npx hoist-ts search GridModel # Ranked search: symbols and members, one line each
npx hoist-ts search headerName # Find which class or config owns a property
npx hoist-ts symbol GridModel # Import line, signature, docs, member summary
npx hoist-ts members GridModel --filter col # Members whose name contains "col", with docsUse search for discovery - one strong keyword works best, ideally an API name
(GridModel, persistWith, headerName); camelCase names match their parts. Multi-word
queries rank hits by how many terms they match, so extra words narrow rather than exclude
("StoreRecord raw" finds StoreRecord.raw, "panel modal" finds ModalSupportModel). Every
hit carries its import path: the package barrel when one exists, otherwise the file. Prefer the
barrel path when shown. Members of every exported class and every *Config, *Spec, and
*Options interface are searched too, so "groupSortFn" reaches both GridModel and
GridConfig; *Props members appear when the query names the component. impl/ and admin/
code is excluded unless you pass --include-internal.
Use symbol when you know the exact name - for classes and interfaces it includes a member
summary, usually enough to write the code - and members with --filter for member docs. When
multiple symbols share a name (e.g. View exists in both cmp/viewmanager and data/cube),
pass the file path with --file to disambiguate - the tools will hint when this is needed. Run
npx hoist-docs --help and npx hoist-ts --help for full usage.
Recommended workflow: Start with the documentation index (hoist-docs index or hoist://docs/index)
to discover available docs. Use the "Quick Reference by Task" table to find the right doc for your
goal, then read the relevant README(s). Supplement with TypeScript symbol lookups for precise API
details. The docs provide architectural context and common pitfalls; the TypeScript tools provide
exact signatures, decorators, and member listings.
A Docker-based server providing GitHub API tools (issues, PRs, code search, etc.) via the
official github-mcp-server image. Configured in .mcp.json but not enabled by default β
it requires Docker and an authenticated GitHub CLI, which not every developer keeps running.
To enable:
- Install and start Docker.
- Install the GitHub CLI (
brew install gh) and authenticate withgh auth login. The server invokesgh auth tokenat startup to fetch a token from the macOS Keychain (orgh's credential store on other platforms), so no plaintext token needs to live in your shell environment. - Add
"github"toenabledMcpjsonServersin.claude/settings.local.json(local settings merge with the sharedsettings.jsonβ enabling locally does not affect other developers):{ "enabledMcpjsonServers": ["hoist-react", "github"] }
If Docker is not running or gh is not authenticated when the server is enabled, Claude Code
may show errors on startup β remove "github" from your local settings to resolve.
Fallback when not enabled: The gh CLI provides functionally equivalent access to the same
operations (gh pr view, gh issue list, gh api, gh pr create, etc.). Prefer gh over
crafting raw curl calls to the GitHub API.
IntelliJ registers its own MCP server in .mcp.json under the name idea, providing tools for
interacting with the IDE (file navigation, code inspections, refactoring, terminal commands, etc.).
It requires a running IDE instance with the MCP server enabled in IntelliJ's settings.
Not enabled by default - the server fails to connect when no IDE is running, which shows as a
startup error. Add "idea" to enabledMcpjsonServers in .claude/settings.local.json to enable it
for yourself (local settings merge with the shared settings.json):
{
"enabledMcpjsonServers": ["hoist-react", "idea"]
}A read-only subset of its tools (search, read, symbol lookup, inspections) is pre-approved in the
shared permissions allowlist, so no extra local config is needed once the server is on. Write and
execute tools - apply_patch, execute_terminal_command, execute_sql_query, the xdebug_*
family - are deliberately left out and still prompt.
IntelliJ rewrites its own entry in .mcp.json on startup. Take its changes rather than reverting
them, or it will keep prompting. Note that it hardcodes the default port 64342, so a second IDE
instance on another port needs a local override.
The shared .claude/settings.json enables XH's xh@hoist-ai plugin, the same plugin that Hoist
applications install. Claude Code offers to install it on your first session here. Three of its
skills apply in this repo:
xh:clear-writing- house style for CHANGELOG entries, PR descriptions, docs, and comments. Prose written here follows its rules. Lint a draft with its script before you commit it.xh:setup-worktree- provisions a runnable worktree. It knows this repo has noclient-app/.xh:using-hoist-react-reference- routes Hoist API questions to the MCP and CLI tools above. It restates what this file already says, so it adds little here. It is the skill that app developers rely on, and this repo is where changes to its tools land first, so keep it enabled.
The other skills are for Hoist applications and do not apply to this library:
xh:onboard-appandxh:hoist-upgradeexpect an app with aclient-app/folder. To write upgrade notes for a hoist-react release, use this repo's ownxh-upgrade-notesskill instead.xh:using-hoist-core-referencecovers Grails and Groovy code, which this repo has none of.
The plugin loads from the marketplace copy, not from a sibling ../hoist-ai checkout. To try a
local skill change, install the plugin from that path as hoist-ai's own CLAUDE.md describes.
pnpm install # Install dependencies
pnpm lint # Lint all code (library JS/TS, MCP tools, SCSS)
pnpm lint:code # Lint library JavaScript/TypeScript only
pnpm lint:mcp # Lint MCP server and CLI tools (mcp/) only
pnpm lint:styles # Lint SCSS only
pnpm typecheck # Type check library and MCP tools
pnpm test:mcp # Run MCP spec scripts, incl. the doc-search golden setLinting and type-checking are separate concerns, and neither subsumes the other β run both. ESLint
is configured type-aware, but that only powers its own rules; it never reports TypeScript compiler
errors, so a genuine type error passes pnpm lint. CI runs the two as distinct steps.
This is a library β it has no dev server or standalone build. To run locally, use a wrapper
application project (e.g., Toolbox) that includes @xh/hoist as a dependency.
The framework is built around three core artifact types:
-
Models (
HoistModel) - State management and business logic classes. Properties are marked with MobX decorators to make them observable. Models form hierarchies reflecting app structure. -
Components (
hoistCmp) - Functional React components wrapped with Hoist support including MobX reactivity and model lookup. Created viahoistCmp.factory({}). -
Services (
HoistService) - Singleton classes for data access and app-wide business logic. Installed viaXH.installServicesAsync()and accessed asXH.myCustomService.
See /core/README.md for detailed coverage of all three artifact types.
XH (in core/XH.ts) is the top-level API entry point. It provides:
- Access to all Hoist services (e.g.,
XH.configService,XH.fetchService,XH.myCustomService) - App metadata (
XH.appCode,XH.appVersion) - Common operations (
XH.toast(),XH.confirm(),XH.handleException())
See /core/README.md for the full XH API and
/svc/README.md for built-in service details.
Hoist strongly encourages rendering components via element factories (created at component
definition time via the hoistCmp.factory util) over JSX. This minimizes XML-style markup and
keeps client side codebases anchored in standard TypeScript/JavaScript syntax:
// Element factory style - strongly preferred
panel({
title: 'Users',
items: [grid({model: gridModel})],
bbar: toolbar(button({text: 'Save'}))
})// JSX also fully supported - but rarely used by XH
< Panel
title="Users"
bbar={<Toolbar><Button text={'Save'}/></Toolbar>}
>
<Grid model={gridModel}/>
</Panel>Factories can take a config object for props, using the key item/items for children. A shortcut
form also exists where factories are passed children directly as arguments, when no other props
are required. Factories all support an omit prop for conditional rendering.
Important β items in, children out: item/items are Hoist's calling API. Inside a
render function, those values arrive as the standard React children prop (because the factory
spreads them as rest args to React.createElement). The canonical pattern when authoring a
container component is therefore to destructure children from props and pass them downstream as
items to an inner factory. See
Authoring a Container Component
in the core README for the full explanation, examples, and the $item/$items escape hatch for
components whose underlying API genuinely has its own items prop.
See /core/README.md for full element factory API including conditional
rendering with omit and factory creation.
All Hoist artifacts extend HoistBase, which provides:
addAutorun()/addReaction()- Managed MobX subscriptions (auto-disposed on destroy)- TC39 decorators - no
makeObservable()call is needed. Declare@observableand@bindablefields with theaccessorkeyword (e.g.@bindable accessor myProp = null). @observableMobX decorator - Marks properties as observable state. Use the MobX 7 named re-exports from@xh/hoist/mobxfor variants - e.g.@observableReffor reference-only.@actionMobX decorator - Marks methods that modify observable state@bindableHoist decorator - Marks properties as observable and generates setter methods automatically marked as@action- e.g.,setMyProp(value)for propertymyProp(Hoist custom decorator).@bindableRefis the reference-only variant. Setter convention: If a class defines an explicit publicsetFoo()method, call it (it likely has additional logic). Otherwise for auto-generated@bindablesetters, prefer direct assignment (model.myProp = value) over calling the generated setter (model.setMyProp(value)).@computedMobX decorator - Marks getter properties as derived/computed state
@manageddecorator - marks child objects for automatic cleanup - apply to properties holdingHoistBaseinstances or arrays of such instances.destroy()- Lifecycle cleanup method.HoistBasesuperclass implementation auto-disposes managed subscriptions and child objects.
See /core/README.md for detailed HoistBase API, persistence support, and
common pitfalls.
- Methods returning Promises are suffixed with
Async(e.g.,loadUsersAsync) - Use the
Runnerchain (this.runner({loadSpec}).linkTo(...).track(...).fetchJson(...)) for masking, activity tracking, and spans - seedocs/telemetry.md - Promise extensions (
catchDefault(),track(),timeout(),linkTo()) are the lower-level API the Runner wraps
Always use Hoist's built-in input components (textInput, numberInput, select, picker,
checkbox, switchInput, dateInput, textArea, etc.) rather than raw HTML <input>,
<select>,
or <textarea> elements. Hoist inputs provide consistent styling, model binding, and proper
integration with the framework's theming and layout system. Raw HTML elements require manual
wrappers and custom SCSS that duplicate what Hoist already provides.
Components in /desktop/ and /mobile/ are platform-specific. Shared code lives in /cmp/,
/core/, /data/, and /svc/.
For the full conventions reference β import organization, class structure, component patterns,
null handling, async patterns, error handling, logging, and CSS naming β see
docs/coding-conventions.md. The principles below are the most
important guidelines to internalize:
- Don't Repeat Yourself β Extract shared logic into utilities, base class methods, or helpers. Balance DRY against readability β extract when a genuine, stable pattern exists, not prematurely.
- Clear, descriptive naming β Names should convey intent and read naturally. Be descriptive but
not verbose (
selectedRecord, notrortheCurrentlySelectedRecordFromTheStore). - Prefer lodash for collection/object utilities β it's null-safe, battle-tested, and aids
readability. Use native JS only when equally expressive (e.g.,
array.map(),array.filter()). - Keep code concise β Favor direct, compact expression over verbose or ceremonial patterns.
Use Hoist's own utilities (
withDefault,throwIf, element factories) to reduce boilerplate. - Named exports only β No default exports. Components export
[Component, factory]pairs from library code, factory only from application/impl code. nulloverundefinedβ Usenullas the "no value" sentinel. Check with== null(loose equality) for concise null-or-undefined testing.- No em dashes - Use
-(spaced hyphen) instead of em dashes (β) in code comments and JSDoc, and in any new prose: CHANGELOG entries, docs, commit messages, PR descriptions. Em dashes cause tooling issues and read as machine-written. Existing docs keep theirs; do not reflow a doc only to remove them. Other Unicode characters (arrows, symbols, accented letters, etc.) are fine in code comments when they aid clarity.
Branching, committing, and pushing all require an explicit ask β never do them unprompted. When it isn't abundantly clear that the user wants one of these, ask first.
Pushing is a deliberate gatekeeping step: never push to any remote unless the user explicitly asks. Some developers hard-block pushes entirely, others allow or request them β so it stays open as a possibility, but always confirm before pushing.
Committing is the most context-dependent of these, varying by developer and by situation. Default to
asking β especially in an interactive session working directly on develop, where each commit is
the developer's call. The exception is orchestrated multi-agent work on a feature branch: when a plan
fans out independent units of work, the go-ahead to commit comes from that plan or orchestration
rather than a per-commit prompt, and agents are expected to make their own discrete, well-scoped
commits as directed.
A skill or third-party plugin instructing you to commit (e.g. "make a small commit after each step") does NOT by itself authorize a commit β that is a default baked into the tool, not the developer's request. This guidance takes precedence: pause and ask. The door stays open for a workflow to commit autonomously, but only when the developer has explicitly opted into that for the workflow at hand β the authorization must come from the developer, not the skill's defaults.
Once the user has asked for a branch (per the "ask first" rule above, don't create one
unprompted): a new branch should map to its own origin/<name> on push β not push into an
existing remote branch.
Default: git switch -c <name> from current HEAD, no base ref. "Make a new branch" means
"from here" β the user is sitting on a particular point in the code; that's the start. If
they want to start from somewhere else (e.g. current origin/develop), they will say so. If
genuinely unclear, ask.
If you do specify a base ref, you MUST pass --no-track. Without it the new branch
silently adopts the base as its upstream, which causes surprise merges on git pull and β
depending on push.default β can push work onto the base branch. Past slips have put
unreviewed work on develop this way.
git switch -c my-feature # β
from current HEAD
git switch -c my-feature --no-track origin/develop # β
explicit base, safe
git switch -c my-feature origin/develop # β auto-tracks develop
git checkout -b my-feature origin/develop # β same trap, checkout spellingIf you forget --no-track: git branch --unset-upstream, then git push -u origin <branch>.
Flag the slip β don't silently fix it. Git prints set up to track 'origin/develop' when this
happens; treat that line as the signal, not as noise.
On feature branches, prefer multiple small commits over amending β PRs are squash-merged into
develop, so intermediate commits are collapsed automatically. Never force-push a feature branch;
if the branch falls behind develop, use a simple merge commit rather than a rebase. Merge commits
and extra commits are harmless on feature branches and are squashed out on merge, while force-pushes
risk losing work and complicate collaboration.
Do not hard-wrap lines at a fixed column width in commit message bodies, pull request descriptions, or issue/PR comments β let the viewing tool handle display wrapping. However, do use line breaks for structure: separate logical points into bullet lists, use blank lines between paragraphs, and break after the subject line. Keep PR descriptions concise β XH developers review these regularly, so favor brief summaries over exhaustive detail. Bullet the key changes and let the diff and any upgrade notes speak for themselves.
Do not add AI-generated attribution to commit messages or PR descriptions β no Generated with ...
line, no π€ Generated with [Claude Code] footer, and no Claude-Session: (or similar
AI-session/attribution) trailer, even if a harness git-instruction block asks for one. XH does not
want these links in the project's history.
Work here often reaches into a sibling checkout - ../toolbox to validate a change against a real
app, or ../hoist-dev-utils when a change touches the build. The rules above apply in every repo
you touch, not just this one β and each sibling has its own CLAUDE.md with additional rules that
bind while you work there. Read it before writing to that repo; the harness only auto-loads the
CLAUDE.md of the primary working directory, so a sibling's rules are never in context by default.
Before adding or editing any entry in CHANGELOG.md (at the repository root), you MUST read and
follow docs/changelog-format.md β the authoritative reference for
section headers, voice, the issue/PR-link policy, and breaking-change requirements. Do not rely on
the summary below alone.
The essentials: new entries go under the topmost -SNAPSHOT version heading, using emoji-prefixed
section headers (e.g. ### π New Features, ### π Bug Fixes). Use past-tense, action-driven
language and name specific classes, methods, and config keys in backticks. Keep entries concise β
one bullet per change, 1-3 lines max. Upgrade notes provide granular detail when needed; the
changelog should not. Do not add GitHub issue or PR links to entries by default β include one only
when explicitly requested or when it points to extensive context that doesn't fit the changelog's
scope (issue/PR references belong in the commit message and PR description). Hard-wrap changelog
entries at 100 characters (unlike commit messages and PR descriptions, which should not be wrapped).
Declaring a new hoist-core minimum: a Requires hoist-core >= X changelog entry is not
self-enforcing. Also update MIN_HOIST_CORE_VERSION in core/XH.ts (checked at startup by
EnvironmentService) and the matching row in
docs/version-compatibility.md. Set the floor only to what the
client genuinely cannot run without - features that detect a missing endpoint and degrade belong in
that doc's Recommended Core column, not the floor.
- MobX - Reactive state management
- AG Grid - Data grid (requires separate license for enterprise features)
- Blueprint - UI component library
- Router5 - Client-side routing
- Highcharts - Charting (requires separate license)
Toolbox is XH's example application showcasing hoist-react patterns and components. It provides real-world usage examples of models, components, services, and other framework features.
- GitHub: https://github.com/xh/toolbox
- Local checkout:
../toolbox(relative to hoist-react root) - likely exists for Hoist library developers only. Note that the client-side code that uses hoist-react is in the../toolbox/client-app/srcdirectory - focus your attention there.
When working on hoist-react library code or documentation, reference Toolbox for practical examples of how features are used in applications. Note that the local checkout is specific to the Hoist development environment and would not be available to general application developers who have hoist-react as a dependency.