Skip to content

Accounts and safety#

The live portal is read-only for nearly the whole engagement. Spend the first day making that hard to get wrong, because care alone won't hold up. Three accounts go into the HubSpot CLI, and every tool that writes refuses any account it doesn't recognise.

In Claude Code

The hubspot-site-setup skill of the bb-hubspot plugin walks through the accounts and the config. The guard hook enforces the account rules on this page for every hs command Claude runs, once hubspot-site.json exists.

The three accounts#

CLI name What it is Allowed
<client>-dev A developer test portal created under the agency's developer account (Enterprise content tier). The CLI default. It expires after 90 idle days. Theme and module uploads, page creation, publishing.
<client>-developer The developer parent account that owns the test portal. Nothing directly. It may own other clients' test portals, so never touch those.
<client>-live The production portal. Crawls, fetches and File Manager reads. One module upload at a time, behind the regression gate. Pages only as drafts, through a service key.

The names and portal ids go into hubspot-site.json (Site config), which every script and the guard hook read.

Setup, in order#

  1. Ask for access on day one. You need a personal access key on the live portal, a developer test portal, and a service key on live with the Content and Files scopes. Access takes longer than anything else.

  2. Authenticate each account by name, with dev as the default.

    hs account auth --pak "<key>" --name <client>-dev --default
    hs account auth --pak "<key>" --name <client>-live
    hs account list
    
  3. Keep keys out of the repo. CLI keys live in ~/.hscli/config.yml. The live service key lives in a git-ignored .env as HUBSPOT_LIVE_TOKEN. .gitignore covers hubspot.config.yml, .hscli/, .env* and backups/ before the first commit. A key pasted into chat gets rotated.

  4. Put the accounts table at the top of CLAUDE.md, together with the rule that nothing is uploaded, not even to dev, unless the client has asked for that step.

  5. Build refusal into every tool that writes. See below.

Refusal, in code#

Every script that writes through the CLI or the API (the publisher, the clone tool, the live-draft patcher, the staging tool) runs the same checks before it sends anything. They live in scripts/hubspot_site.py and read hubspot-site.json:

site = load()                        # hubspot-site.json, found by walking up from the current directory
acct = site.account('live')          # 1. only the site's own accounts; anything else exits
site.verify_cli(acct)                # 2. `hs account list` must show this name resolving to this portal id
site.check_sentinel(acct, api)       # 3. the sentinel page must exist on the portal the key actually reaches

The guard hook adds the same account rules to every hs command Claude runs by hand.

Live mode is draft-only, and that is in the code, not a convention: on live the publisher has no schedule call at all. If a page already exists at the target slug on live and the tool didn't create it, the publisher refuses to touch it.

Why it is built this way#

The CLI falls through silently

hs … --account <name> with a name that isn't in the config uses the default account without saying so. In practice, a mistyped account name returned another client's portal data. An upload aimed at one portal can land on another, which is why every writing tool checks hs account list and a sentinel page first.

A personal access key on production is read-only for pages

A personal access key on a production portal only offers CMS Pages read. Creating drafts on live needs a service key (Settings › Integrations › Private Apps › Use Service Keys) with Content and Files scopes, plus Forms if pages carry forms. The CLI key still handles fetches, File Manager uploads and module uploads.

Standing rules#

  • Run hs account list before any CLI write and always pass --account explicitly.
  • Uploads to dev happen only when the client asks for them, never as part of building a preview.
  • Live module uploads go one module at a time, each behind the visual-regression gate.
  • Pages on live are created or patched as drafts. A person clicks Publish.
  • Log every upload and publish in CLAUDE.md with the date, the ids and what changed.