contentrain-normalize — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited contentrain-normalize (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Normalize converts a codebase with hardcoded strings into a Contentrain-managed content architecture. It runs in two independent phases, each producing a separate branch for review.
.contentrain/ structure. Source files are NOT modified.Phase 1 alone is valuable: content becomes manageable in Studio, translatable, and publishable without touching source code.
contentrain_scan → contentrain_apply)dry_run: true before dry_run: false for every contentrain_apply callnpx contentrain generate after Phase 2 completes.contentrain/ files via reuse (content files are read-only for reuse)contentrain_apply callNormalize (contentrain_scan and contentrain_apply) requires local disk access — AST scanners walk the source tree and patch files in place. It runs only on a LocalProvider (stdio transport, or an HTTP transport configured with a LocalProvider).
Remote providers (GitHubProvider, GitLabProvider, future BitbucketProvider) expose astScan: false, sourceRead: false, and sourceWrite: false. Calling these tools over a remote provider returns a uniform capability error:
{
"error": "contentrain_scan requires local filesystem access.",
"capability_required": "astScan",
"hint": "This tool is unavailable when MCP is driven by a remote provider. Use a LocalProvider or the stdio transport."
}If the agent is driving a remote-only MCP session, normalize must run in a separate local-checkout session before the extracted content branch is pushed.
| Aspect | Phase 1: Extraction | Phase 2: Reuse |
|---|---|---|
| Purpose | Pull content from source to .contentrain/ | Patch source files with content references |
| Scope | Full project scan | Per model or per domain |
| Source files modified | No | Yes |
| Branch pattern | cr/normalize/extract/{domain}/{timestamp} | cr/normalize/reuse/{model}/{locale}/{timestamp} |
| Prerequisite | Initialized .contentrain/ | Completed extraction (content exists in .contentrain/) |
| Workflow mode | Always review | Always review |
| Standalone value | Yes -- content is manageable in Studio immediately | Depends on Phase 1 |
.contentrain/.MCP is framework-agnostic. It does NOT know how {t('key')} differs from {{ $t('key') }}. The agent provides all stack-specific logic.
| Guardrail | Limit | Rationale |
|---|---|---|
| Allowed source file types | .vue, .tsx, .jsx, .ts, .js, .mjs, .astro, .svelte | Only scan files that can contain UI content |
| Max files per scan | 500 | Prevent scanning entire monorepos |
| Max files per apply | 100 | Keep diffs reviewable |
| Dry-run before apply | MANDATORY | Preview all changes before executing |
| Workflow mode | Always review | Normalize changes are never auto-merged |
| Reuse scope | Per model or per domain | No whole-project reuse in one operation |
| Reuse prerequisite | Extraction must be complete | Content must exist in .contentrain/ before patching source |
Attempting to apply without a prior dry-run will be rejected. Exceeding file limits will truncate results with a warning.
Call contentrain_status to confirm the project is initialized and note supported locales. Call contentrain_describe_format to understand the four storage formats (dictionary, collection, singleton, document) before deciding model structure.
Call contentrain_scan(mode: "graph") to build the import/component dependency graph. Use this to understand which components belong to which pages, identify shared vs page-specific components, and prioritize files.
Call contentrain_scan(mode: "candidates") to find hardcoded strings. The scan returns candidates with file paths, line numbers, string values, and surrounding context.
marketing, blog, ui, system).Call contentrain_apply(mode: "extract", dry_run: true) to generate a preview. Review the dry-run output: verify model definitions, content assignments, and check for missed or misclassified candidates. Show the preview to the user.
After user approval, call contentrain_apply(mode: "extract", dry_run: false). This creates model definitions and content files in .contentrain/ on a cr/normalize/extract/{timestamp} branch. Source files are NOT modified.
Call contentrain_validate to check schema compliance, i18n completeness, and duplicate entries. Call contentrain_submit to push the branch for review. Normalize operations always use review workflow mode.
Tell the user: "Phase 1 complete. Content is now in Contentrain and can be managed, translated, and published from Studio. When ready, proceed with Phase 2 to update source files."
Two paths — agent chooses based on context:
Path A — Browser Review (recommended for visual diff):
contentrain serve is running (or ask user to start it)Path B — Terminal Review:
contentrain_apply(mode: 'extract', dry_run: false) → executecontentrain_validate → contentrain_submitcontentrain_merge(branch: 'cr/normalize/extract/...', confirm: true) to merge locallyPhase 2 CANNOT start until Phase 1 branch is merged. Three options:
contentrain_merge(branch: 'cr/normalize/extract/...', confirm: true)Verify with contentrain_status — check that extract branch is no longer in unmerged list.
Before starting reuse:
contentrain_status and verify extract branch is mergedAfter all reuse operations are complete, source files now use #contentrain imports. The SDK client MUST be regenerated:
npx contentrain generateWithout this, #contentrain imports will fail at build time. See: contentrain-generate skill for details.
Only start after Phase 1 is reviewed and merged.
List the extracted models and ask the user which model or domain to process first. Process one model or domain at a time to keep diffs small and reviewable.
Based on the project's tech stack, determine the correct replacement pattern. See Reuse Details for the full replacement expressions table by stack. Also determine any necessary import statements for patched files.
Call contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: true). Review: verify each replacement is correct, import statements will be added, no non-content strings are being replaced, component structure is preserved.
After user confirmation, call contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: false). This patches source files and creates a cr/normalize/reuse/{model}/{timestamp} branch.
Call contentrain_validate to verify source files parse correctly, content references resolve, and no strings were missed or double-replaced. Call contentrain_submit to push the branch for review.
Ask the user which model or domain to process next. Repeat steps 1-5 for each remaining model until all extracted content is referenced in source code.
| Content Location | Suggested Domain |
|---|---|
| Landing page, marketing sections | marketing |
| Blog, articles, posts | blog |
| Navigation, footer, header | ui |
| Error messages, validation | system |
| Product pages, e-commerce | product |
| Documentation, help | docs |
| User-facing app strings | app |
| Content Pattern | Kind | Rationale |
|---|---|---|
| One set of fields per page section | singleton | Hero, features, pricing header |
| Multiple items of same type | collection | Team members, FAQs, testimonials |
| Long-form with metadata | document | Blog posts, documentation pages |
| Key-value UI strings | dictionary | Error messages, button labels, form labels |
| Mistake | Correct Approach |
|---|---|
| Extracting CSS values as content | Only extract user-visible text |
| Creating one model per component | Group related content into shared models |
| Skipping dry-run | ALWAYS preview before apply |
| Auto-merging normalize changes | Normalize ALWAYS uses review mode |
| Reusing before extraction is merged | Wait for extraction review and merge first |
| Processing all models in one reuse | Scope reuse to one model/domain at a time |
| Ignoring project graph | Use graph output to understand component relationships |
| Hardcoding replacement patterns | Detect the project's i18n stack and use its conventions |
| Extracting strings with runtime variables | Split parameterized strings: extract the static part, leave interpolation in code |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.