Your HubSpot portal, in a pull request.
Describe properties and objects in TypeScript. Kalup shows every change as a plan and applies it to any portal you name.
npx kalup init --portal 1111111kalup/ˈka.lup/ · noun · SlovenianA mould: the form you pour metal into. Your config is the mould. Kalup pours your portal into it.
Where Kalup sits.
Between the files in your repository and your HubSpot portals. Every change goes through a plan you can read before anything is written.
defineObject('companies', {name: p.string(...),billingStatus: p.enum(...),})
written by kalup pull
- Name
- Billing status
- Name
- Billing status
protected · applies need a person at a terminal
No file.The config lives in the portal and nowhere else.
No diff.A change lands the moment someone clicks save.
No history.The audit log API needs Enterprise. Below that, history is what people remember.
And now AI agents can change your portal from a prompt. A prompt is not a review.
Read the plan, not the config.
Every step names the resource, the risk and the exact values before and after. Titles use the words of the HubSpot UI, so an admin can approve a change without reading TypeScript.
config "Billing status" · portal "Billing state"
Apply re-checks the portal right before each write. If it changed since you approved, the step stops.
$ kalup plan --target sandbox Reading portal 1111111 (sandbox) ... done Plan for sandbox: 2 steps · 2 safe · 1 held + Create company property "Renewal date" safe property:companies/renewal_date ~ Edit company property "Billing status" safe + option "Past due" held label (drift): config "Billing status", portal "Billing state" Nothing was written. Review, then apply. $
Nothing changes behind your back.
People keep editing the portal in the HubSpot UI, and that is fine. Kalup holds those edits instead of overwriting them, and deleting a line from a file deletes nothing.
Edits in the UI are held.
billingStatus: p.enum('billing_status', {
label: 'Billing status',
...
})config "Billing status" · portal "Billing state" · not written
A delete needs four keys.
- key 1Tombstonekalup rm writes it to kalup/removed.ts
- key 2Ownershipstate shows Kalup created it in this target
- key 3Policythe target allows destroys
- key 4A personat a terminal, typing the count
All four keys. Only now can the delete run.
Let the agent do the typing. Keep the approval.
Kalup is built to be driven by Claude Code and other agents, and built so they cannot approve for you.
- Speaks JSON.Every command takes --json, with fixed exit codes and errors that name the file, line and fix.
- Writes its own rules.kalup init writes an AGENTS.md: edit config and plan, never write to the portal directly.
- Stops at production.A protected target needs a person at a real terminal typing its name. An agent cannot say yes for you.
- Reads, never obeys.Text read from the portal is data, never instructions.
$ kalup apply plan.json --json { "format": "envelope/1", "ok": false, "issues": [{ "code": "E_TTY_REQUIRED", "message": "Applying to production needs a person at a terminal.", "fix": "Open a new terminal window and run: kalup apply plan.json", "humanRequired": true }] } exit 4 · a person is needed $
The file that shapes the portal also types your app.
Import your object files and get exact types and codecs. No generate step, no hand-typed property names drifting away from the portal.
// kalup/objects/companies.ts
import { defineObject, p, type InferProperties } from '@kalup/core'
export const Company = defineObject('companies', {
groups: { billing: { label: 'Billing' } },
properties: {
billingStatus: p.enum('billing_status', {
label: 'Billing status',
group: 'billing',
fieldType: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'PAST DUE', label: 'Past due', as: 'past_due' },
],
}).required(),
},
})
export type CompanyData = InferProperties<typeof Company.properties>company.billingStatus'PAST DUE' in the portal is 'past_due' in your code. The codec translates both ways.
Three ways in.
Same files, same plan, three daily flows. Move your cursor over the metal.
Developers
/developersShip the property before the code that needs it. CI plans on the pull request and applies after merge.
Read the flow →Agents
/agentsFor admins working through Claude Code. Ask for the change, read the plan, say yes.
Read the flow →Agencies
/agenciesEvery client portal pulled into files, documented, and compared against its sandbox.
Read the flow →When HubSpot has no API, Kalup says so.
Some settings can only be changed in the UI. For those, the plan prints a runbook with the page, the fields and the values, and a person does it. Kalup never claims a change it could not make.
See full coverage- 1. Open the settings for the deal pipeline "Enterprise".
- 2. Edit the stage "Closed won".
- 3. Make "Renewal date" and "Billing status" required on that stage.
- verify: both properties show as required on the stage
Free, and it stays free.
Apache-2.0. Everything that runs on your machine or in your CI against HubSpot's public APIs is free and stays free. The licence will not tighten.
- Pull and validateYour portal, in files
- Compare and planEvery change, in words
- Apply and CIReviewed writes only
- Typed clientRecords, typed by the same files