From hubspot-pack
Enforces HubSpot security in JS/TS projects via CI secret scanning for tokens/API keys, ESLint rules against deprecated auth, and .gitignore checks.
How this skill is triggered — by the user, by Claude, or both
Slash command
/hubspot-pack:hubspot-policy-guardrailsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Automated policy enforcement for HubSpot integrations: secret scanning, ESLint rules, CI checks for token leaks, and runtime guardrails.
Automated policy enforcement for HubSpot integrations: secret scanning, ESLint rules, CI checks for token leaks, and runtime guardrails.
# .github/workflows/hubspot-security.yml
name: HubSpot Security Scan
on: [push, pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for HubSpot private app tokens
run: |
# Pattern: pat-{region}{number}-{uuid}
if grep -rE "pat-[a-z]{2}[0-9]-[a-f0-9-]{36}" \
--include="*.ts" --include="*.js" --include="*.json" --include="*.yaml" \
--exclude-dir=node_modules --exclude-dir=.git .; then
echo "::error::HubSpot private app token found in source code!"
echo "Rotate this token immediately in HubSpot Settings > Private Apps"
exit 1
fi
- name: Scan for deprecated API keys
run: |
# Pattern: hapikey=uuid
if grep -rE "hapikey=[a-f0-9-]{36}" \
--include="*.ts" --include="*.js" --include="*.env.example" \
--exclude-dir=node_modules .; then
echo "::error::Deprecated HubSpot API key found! Migrate to private app tokens."
exit 1
fi
- name: Verify .gitignore includes .env files
run: |
if ! grep -q "^\.env$" .gitignore; then
echo "::error::.gitignore missing .env entry"
exit 1
fi
// eslint-rules/no-hubspot-api-key.js
module.exports = {
meta: {
type: 'problem',
docs: { description: 'Disallow deprecated HubSpot API key authentication' },
messages: {
noApiKey: 'HubSpot API keys are deprecated. Use accessToken from a private app instead.',
useAccessToken: 'Use { accessToken: process.env.HUBSPOT_ACCESS_TOKEN } instead of { apiKey }',
},
},
create(context) {
return {
Property(node) {
if (
node.key.type === 'Identifier' &&
node.key.name === 'apiKey' &&
node.parent?.parent?.callee?.name === 'Client'
) {
context.report({ node, messageId: 'noApiKey' });
}
},
Literal(node) {
if (typeof node.value === 'string') {
// Detect hardcoded private app tokens
if (node.value.match(/^pat-[a-z]{2}\d-[a-f0-9-]{36}$/)) {
context.report({
node,
message: 'Hardcoded HubSpot access token detected. Use environment variable.',
});
}
// Detect deprecated API keys
if (node.value.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) &&
node.parent?.key?.name === 'apiKey') {
context.report({ node, messageId: 'useAccessToken' });
}
}
},
};
},
};
// src/hubspot/strict-types.ts
// Enforce that all HubSpot operations go through the service layer
// (not raw client calls scattered throughout the codebase)
// BAD: Using raw client anywhere
// const client = new hubspot.Client({ accessToken: '...' });
// client.crm.contacts.basicApi.create(...); // unguarded
// GOOD: All operations through typed service
interface IContactService {
findByEmail(email: string): Promise<Contact | null>;
create(data: CreateContactInput): Promise<Contact>;
update(id: string, data: UpdateContactInput): Promise<Contact>;
archive(id: string): Promise<void>;
}
// Enforce required properties at compile time
interface CreateContactInput {
email: string; // required
firstname?: string;
lastname?: string;
lifecyclestage?: 'subscriber' | 'lead' | 'marketingqualifiedlead' |
'salesqualifiedlead' | 'opportunity' | 'customer' | 'evangelist';
}
// Prevent passing unknown/dangerous properties
type UpdateContactInput = Partial<Omit<CreateContactInput, 'email'>>;
// Compile-time check: lifecycle stage must be valid
const validStage: CreateContactInput = {
email: '[email protected]',
lifecyclestage: 'customer', // TypeScript enforces valid values
};
// This would fail at compile time:
// lifecyclestage: 'invalid_stage' // Type error
// Prevent dangerous operations in production
const BLOCKED_IN_PROD: Record<string, string> = {
'batch/archive': 'Bulk archiving is blocked in production',
'gdpr-delete': 'GDPR delete requires manual approval in production',
};
function guardOperation(path: string): void {
if (process.env.NODE_ENV !== 'production') return;
for (const [pattern, message] of Object.entries(BLOCKED_IN_PROD)) {
if (path.includes(pattern)) {
throw new Error(`BLOCKED: ${message}. Environment: production.`);
}
}
}
// Rate limit self-protection (don't consume entire portal quota)
class SelfRateLimiter {
private callsThisSecond = 0;
private callsToday = 0;
private lastSecond = Math.floor(Date.now() / 1000);
private lastDay = new Date().toDateString();
private maxPerSecond: number;
private maxPerDay: number;
constructor(maxPerSecond = 8, maxPerDay = 400000) {
this.maxPerSecond = maxPerSecond; // leave 2/sec headroom for other apps
this.maxPerDay = maxPerDay; // leave 100K/day headroom
}
check(): void {
const now = Math.floor(Date.now() / 1000);
const today = new Date().toDateString();
if (now !== this.lastSecond) {
this.callsThisSecond = 0;
this.lastSecond = now;
}
if (today !== this.lastDay) {
this.callsToday = 0;
this.lastDay = today;
}
this.callsThisSecond++;
this.callsToday++;
if (this.callsThisSecond > this.maxPerSecond) {
throw new Error(
`Self rate limit: ${this.callsThisSecond}/${this.maxPerSecond} per second`
);
}
if (this.callsToday > this.maxPerDay) {
throw new Error(
`Self rate limit: ${this.callsToday}/${this.maxPerDay} per day`
);
}
}
}
#!/bin/bash
# .husky/pre-commit
# Scan for HubSpot tokens in staged files
PATTERN="pat-[a-z]{2}[0-9]-[a-f0-9-]{36}"
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "\.(ts|js|json|yaml|yml)$")
if [ -n "$STAGED_FILES" ]; then
if echo "$STAGED_FILES" | xargs grep -lE "$PATTERN" 2>/dev/null; then
echo "ERROR: HubSpot access token found in staged files!"
echo "Remove the token and use environment variables instead."
exit 1
fi
fi
| Issue | Cause | Solution |
|---|---|---|
| False positive on UUID | Pattern too broad | Check context (parent node name) |
| Token leaked to git | Pre-commit hook skipped | Enforce in CI as backup |
| Self-limiter too strict | Conservative defaults | Adjust based on portal usage |
| ESLint rule not running | Plugin not registered | Add to .eslintrc plugins array |
For architecture blueprints, see hubspot-architecture-variants.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin hubspot-packSets up GitHub Actions CI/CD for HubSpot integrations with unit/integration tests, test account secrets, and API verification in Node.js projects.
Provides expert patterns for HubSpot CRM integration: OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js and Python SDKs.
Blocks unsafe code before commit with secret scanning, OWASP Top 10 detection, dependency audits (npm/pip/cargo), and permission checks. Hard security gate on critical findings.