SVG currentColor
After an agent writes an SVG, normalize hardcoded stroke and fill colors to currentColor so icons inherit theme text color.
$ npx ted-craft add svg-current-color -a cursor -y# SVG currentColor
After an agent writes an SVG file, this hook rewrites hardcoded `stroke` and `fill` colors to `currentColor` so icons pick up your theme text color.
## Why use it
Agents often emit SVGs with fixed hex colors. Those icons ignore dark mode and parent text color. With `currentColor`, the SVG follows CSS `color` like the rest of your UI.
## Install
This is a **hook**, not a skill — you still add it with `ted-craft add` (same CLI). Run the command in the **app project** where you want the hook (the folder that should get `.cursor/`), not as a global skill install.
```bash
npx ted-craft add svg-current-color -a cursor -y
```
That writes into the current project:
- `.cursor/hooks.json` — registers the `afterFileEdit` hook (merged with any hooks you already have)
- `.cursor/hooks/svg-current-color.mjs` — the normalizer script
Pass `-g` only if you intentionally want Cursor’s user-level hooks directory. The packaged command path is project-relative (`.cursor/hooks/...`), so project install is the usual choice.
### Manual install
If the CLI is unavailable, copy from the registry package:
1. Create `.cursor/hooks/` in your project
2. Copy `svg-current-color.mjs` into `.cursor/hooks/`
3. Merge this entry into `.cursor/hooks.json` (keep any hooks you already have):
```json
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "node .cursor/hooks/svg-current-color.mjs",
"matcher": "Write"
}
]
}
}
```
## What it does
On Cursor `afterFileEdit` when the edit matcher is `Write`:
1. Reads the edited path from the hook payload
2. Skips anything that is not a `.svg` under a workspace root
3. Replaces hardcoded `stroke` / `fill` attributes and simple inline styles with `currentColor`
4. Leaves `none` and existing `currentColor` values alone
## Safety
This hook is local-only by design:
- No network, no shell, no downloads
- Writes only the edited SVG, and only if it resolves inside a workspace root
- Skips symlinks that point outside the workspace
- Skips files larger than 2 MiB
- Exits quietly on errors so it never blocks the agent
Review the script on this page before you install — hooks run on your machine.
## Caveats
- Runs on `Write` via `afterFileEdit` only
- If you need brand-specific SVG colors, keep them out of agent-generated icons or re-apply them after the hook runs
- `fill="none"` is preserved so hollow icons stay hollowSVG currentColor
After an agent writes an SVG file, this hook rewrites hardcoded stroke and fill colors to currentColor so icons pick up your theme text color.
Why use it
Agents often emit SVGs with fixed hex colors. Those icons ignore dark mode and parent text color. With currentColor, the SVG follows CSS color like the rest of your UI.
Install
This is a hook, not a skill — you still add it with ted-craft add (same CLI). Run the command in the app project where you want the hook (the folder that should get .cursor/), not as a global skill install.
npx ted-craft add svg-current-color -a cursor -yThat writes into the current project:
.cursor/hooks.json— registers theafterFileEdithook (merged with any hooks you already have).cursor/hooks/svg-current-color.mjs— the normalizer script
Pass -g only if you intentionally want Cursor’s user-level hooks directory. The packaged command path is project-relative (.cursor/hooks/...), so project install is the usual choice.
Manual install
If the CLI is unavailable, copy from the registry package:
- Create
.cursor/hooks/in your project - Copy
svg-current-color.mjsinto.cursor/hooks/ - Merge this entry into
.cursor/hooks.json(keep any hooks you already have):
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "node .cursor/hooks/svg-current-color.mjs",
"matcher": "Write"
}
]
}
}What it does
On Cursor afterFileEdit when the edit matcher is Write:
- Reads the edited path from the hook payload
- Skips anything that is not a
.svgunder a workspace root - Replaces hardcoded
stroke/fillattributes and simple inline styles withcurrentColor - Leaves
noneand existingcurrentColorvalues alone
Safety
This hook is local-only by design:
- No network, no shell, no downloads
- Writes only the edited SVG, and only if it resolves inside a workspace root
- Skips symlinks that point outside the workspace
- Skips files larger than 2 MiB
- Exits quietly on errors so it never blocks the agent
Review the script on this page before you install — hooks run on your machine.
Caveats
- Runs on
WriteviaafterFileEditonly - If you need brand-specific SVG colors, keep them out of agent-generated icons or re-apply them after the hook runs
fill="none"is preserved so hollow icons stay hollow
Hook config
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "node .cursor/hooks/svg-current-color.mjs",
"matcher": "Write"
}
]
}
}Hook script: svg-current-color.mjs
#!/usr/bin/env node
/**
* SVG currentColor normalizer (Cursor afterFileEdit hook).
*
* Threat model: local-only. Reads stdin JSON from Cursor, and may rewrite a
* single .svg file under a workspace root. No network, no shell, no eval.
* Preserves fill="none" and values already set to currentColor.
*/
import { readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
import { isAbsolute, join, resolve, sep } from "node:path";
const RESERVED = new Set(["none", "currentcolor", "currentColor"]);
const MAX_BYTES = 2 * 1024 * 1024;
/**
* @param {unknown} input
* @returns {string | null}
*/
function resolveFilePath(input) {
if (!input || typeof input !== "object") return null;
const raw = /** @type {{ file_path?: unknown }} */ (input).file_path;
if (typeof raw !== "string" || !raw.trim()) return null;
if (isAbsolute(raw)) return resolve(raw);
const roots = /** @type {{ workspace_roots?: unknown }} */ (input).workspace_roots;
const root = Array.isArray(roots) && typeof roots[0] === "string" ? roots[0] : null;
return root ? resolve(join(root, raw)) : resolve(raw);
}
/**
* @param {string} filePath
* @param {unknown} input
* @returns {boolean}
*/
function isInsideWorkspace(filePath, input) {
const roots = /** @type {{ workspace_roots?: unknown }} */ (input).workspace_roots;
if (!Array.isArray(roots) || roots.length === 0) return false;
let realFile;
try {
// Follows symlinks — rejects targets outside the workspace.
realFile = realpathSync(filePath);
} catch {
// Missing path: jail on the lexical absolute path.
realFile = resolve(filePath);
}
for (const root of roots) {
if (typeof root !== "string" || !root.trim()) continue;
let realRoot;
try {
realRoot = realpathSync(root);
} catch {
realRoot = resolve(root);
}
const prefix = realRoot.endsWith(sep) ? realRoot : realRoot + sep;
if (realFile === realRoot || realFile.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* @param {string} value
*/
function isReserved(value) {
return RESERVED.has(value.trim());
}
/**
* @param {string} svg
*/
function normalizeSvgColors(svg) {
let result = svg;
result = result.replace(
/\bstroke=(["'])([^"']*)\1/gi,
(_, quote, value) =>
isReserved(value) ? `stroke=${quote}${value}${quote}` : 'stroke="currentColor"',
);
result = result.replace(
/\bfill=(["'])([^"']*)\1/gi,
(_, quote, value) =>
isReserved(value) ? `fill=${quote}${value}${quote}` : 'fill="currentColor"',
);
result = result.replace(
/\bstroke:\s*(#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|[a-z]+)\s*(;|(?=\s|"))/gi,
(match, color, tail) =>
isReserved(color) ? match : `stroke:currentColor${tail}`,
);
result = result.replace(
/\bfill:\s*(#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|[a-z]+)\s*(;|(?=\s|"))/gi,
(match, color, tail) =>
isReserved(color) ? match : `fill:currentColor${tail}`,
);
return result;
}
function main() {
try {
const input = JSON.parse(readFileSync(0, "utf8"));
const filePath = resolveFilePath(input);
if (!filePath || !filePath.toLowerCase().endsWith(".svg")) {
process.exit(0);
}
if (!isInsideWorkspace(filePath, input)) {
process.exit(0);
}
let st;
try {
st = statSync(filePath);
} catch {
process.exit(0);
}
if (!st.isFile() || st.size > MAX_BYTES) {
process.exit(0);
}
const before = readFileSync(filePath, "utf8");
const after = normalizeSvgColors(before);
if (after !== before) {
writeFileSync(filePath, after, "utf8");
}
} catch {
// Fail closed: never break the agent loop.
}
process.exit(0);
}
main();