sf-compact
Cut Salesforce metadata tokens in half for AI coding agents.
Salesforce metadata XML is extremely verbose — profiles, permission sets, flows, and objects can be 20,000-50,000+ lines of XML with 70-85% structural overhead. Every time an AI agent reads your metadata, you're paying for tokens that carry no useful information.
sf-compact converts metadata XML to compact YAML/JSON. In controlled benchmarks (3 runs each, same task), compact files reduced Claude Code costs by 11.5% — and by 33% when combined with a custom exploration agent.
Quick Start
That's it. init instructions injects a directive into your CLAUDE.md (or equivalent AI tool file) telling the AI to read from .sf-compact/ instead of force-app/. In our benchmarks, this was the simplest approach that reliably worked.
Why This Approach (Benchmarks)
We tested 17 different approaches over 6 weeks on a production Salesforce org (~10,000 files, 42MB). Most failed or made things worse. Here's what we learned:
What works:
- CLAUDE.md directive (
init instructions): -11.5% cost, statistically verified across 3 runs. Sub-agents DO follow CLAUDE.md — an earlier assumption that they don't was wrong. - Custom sf-explorer agent (
init agent): -32.9% cost in a single-run test. Routes metadata reads through a Haiku-based agent optimized for.sf-compact/.
What doesn't work:
- Mentioning sf-compact in the prompt without setup: +22.9% cost. The AI wasted turns discovering the tool.
- Hooks (5 variants tested): -1% to +29% cost. Technically functional but unnecessary — CLAUDE.md already redirects reads, so hooks have nothing to intercept.
- MCP tools alone: -2.6% cost. The AI never voluntarily called the tools.
Why YAML is the default format:
Token savings vary dramatically by metadata type. JSON saves 31-34% on large types (profiles, objects, record types) but is actually worse for small types — fields (+0.4%), list views (+1.1%), compact layouts (+5.5%). The JSON _children/_tag/_text overhead exceeds the XML tag overhead on small files. YAML avoids this and works well across all types.
Full benchmark details: Medium article
Output Formats
| Format | Preserves order | Human-readable | Best for |
|---|---|---|---|
yaml |
No | Yes | Most types (default) |
yaml-ordered |
Yes | Yes | Order-sensitive types (Flow, FlexiPage, Layout) |
json |
Yes | Less | Maximum savings on large types (profiles, objects) |
Before / After
XML (848 tokens):
false
Salesforce
true
Account.AnnualRevenue
true
...
YAML (432 tokens -- 49% reduction):
_tag: Profile
_ns: http://soap.sforce.com/2006/04/metadata
custom: false
userLicense: Salesforce
fieldPermissions:
- editable: true
field: Account.AnnualRevenue
readable: true
...
Install
npm (recommended)
Homebrew (macOS / Linux)
From crates.io
From source
Commands
Pack (XML -> compact format)
Unpack (compact format -> XML)
Watch (auto-pack on changes)
Stats (preview savings)
Diff / Lint / Changes
Configuration
Default config: yaml for all types, with yaml-ordered overrides for order-sensitive types (Flow, FlexiPage, Layout) to preserve element order.
AI Tool Integration
init instructions (recommended) — injects a directive block into your AI tool's instruction file. Auto-detects which tools are configured. This is the simplest approach and sufficient for the -11.5% cost saving measured in benchmarks.
Supported targets: claude, cursor, copilot, codex, windsurf, cline, aider, stdout.
init agent — creates a .claude/agents/sf-explorer.md custom agent that reads metadata from .sf-compact/. In benchmarks, Opus + sf-explorer delegation showed -32.9% cost reduction. Add a CLAUDE.md directive to delegate metadata exploration to sf-explorer for best results.
init hook — installs a Claude Code PreToolUse hook. In our benchmarks, hooks were unnecessary because CLAUDE.md already redirected reads. Included for environments where CLAUDE.md is not available.
init mcp — exposes sf-compact tools via MCP. In benchmarks, the AI did not voluntarily use MCP tools without explicit instructions.
Manifest
Output supported metadata types in JSON:
Workflow
- Configure (once):
sf-compact config init - Pull metadata:
sf project retrieve start - Pack:
sf-compact pack - Setup AI:
sf-compact init instructions(once per project) - Work -- AI reads compact files via CLAUDE.md directive
- Unpack:
sf-compact unpack-- restores XML - Deploy:
sf project deploy start
Use sf-compact watch during development to auto-pack on changes, and sf-compact lint in CI to ensure compact files stay in sync.
Tip: Add
.sf-compact/to.gitignoreif you treat it as a build artifact, or commit it for AI-friendly diffs.
What "Semantically Lossless" Means
The roundtrip preserves all data that Salesforce cares about:
- Whitespace -- leading/trailing whitespace in text nodes is trimmed
- Comments -- stripped (use
--preserve-commentsto keep them) - CDATA -- unwrapped to escaped text (
<,&) - Empty elements --
<tag></tag>may become<tag/> - Element order -- may change with
yamlformat; useyaml-orderedorjsonto preserve
Supported Metadata Types
76 file extensions mapping to Salesforce metadata types across 10 categories:
| Category | Types |
|---|---|
| Security | Profile, PermissionSet, PermissionSetGroup, RemoteSiteSetting, CspTrustedSite, ConnectedApp, SharingRules, CustomPermission, Role, Group, AuthProvider, SamlSsoConfig, Certificate |
| Schema | CustomObject, CustomField, ValidationRule, CustomMetadata, GlobalValueSet, StandardValueSet, RecordType, MatchingRule, DuplicateRule, CustomIndex, FieldSet |
| Code | ApexClass, ApexTrigger, ApexComponent, ApexPage, LightningComponentBundle (js/css/html/xml), AuraDefinitionBundle (cmp/evt), StaticResource |
| Automation | Flow*, Workflow, WorkflowRule, AssignmentRules, AutoResponseRules, EscalationRules |
| UI | Layout*, CustomLabels, CustomApplication, CustomTab, FlexiPage*, CustomSite, QuickAction, PathAssistant, ListView, CompactLayout, WebLink, HomePageLayout, AppMenu, Community, Letterhead |
| Analytics | ReportType, Report, Dashboard |
| Integration | ExternalServiceRegistration, NamedCredential, ExternalCredential |
| Config | Settings, InstalledPackage, TopicsForObjects, CustomNotificationType, CleanDataService, NotificationTypeConfig, PlatformEventChannelMember |
| Translation | CustomObjectTranslation, CustomFieldTranslation |
| Content | EmailTemplate, ManagedContentType, IframeWhiteListUrlSettings, LightningMessageChannel |
* Order-sensitive types -- config init defaults these to yaml-ordered to preserve element order.
How It Works
- Parses Salesforce metadata XML into a tree structure
- Groups repeated elements (e.g.,
<fieldPermissions>) into arrays - Coerces booleans:
"true"->true,"false"->false. All other values preserved as-is - Flattens simple key-value containers into inline mappings
- Preserves namespaces, attributes, and all structural information
- Order-sensitive types default to
yaml-orderedformat with_childrensequences
Token counting uses the cl100k_base tokenizer (same family used by GPT-4 and Claude).
License
MIT