Denies implementer file edits on the test tree and test-author file edits on production code, so each agent stays on its side of the test boundary.

<img src="assets/personal-logo.png" alt="Personal logo" width="160">
A collection of skills and custom agents for Claude Code, Codex, and Cursor CLI. Claude Code and Cursor install three domain plugins; Codex installs the shared skills directly from this checkout.
Skills ship in three plugins, one per domain. A skill is invoked as /<group>:<skill-name> — the prefix is the plugin it came from, and it is not optional.
craft — how code gets written and checked| Skill | Invoke | Description |
|---|---|---|
architecture-baseline | /craft:architecture-baseline | Decide a new project's architectural constraints before feature work. Requires the separately installed researcher agent. |
python-engineering-standards | /craft:python-engineering-standards | Apply production Python standards for layout, typing, configuration, logging, error handling, testing, and packaging. |
code-audit | /craft:code-audit | Audit code in any language and write a machine-readable report covering correctness, security, performance, architecture, error handling, and readability. |
structure-review | /craft:structure-review | Review the shape and project conformance of a finished change before merge or archive. |
flow — how work gets driven from idea to build| Skill | Invoke | Description |
|---|---|---|
scout | /flow:scout | Explore work whose shape is not settled and return a decision-ready briefing. Requires the separately installed pathfinder and researcher agents. |
grill-me | /flow:grill-me | Pressure-test ideas and change artifacts before implementation. |
orchestrate | /flow:orchestrate | Drive workers to implement a bounded plan. Requires the separately installed implementer, pathfinder, and researcher agents. |
write-ticket | /flow:write-ticket | Write Jira tickets and comments in plain language through the Atlassian MCP. |
stash | /flow:stash | Park raw content in an Obsidian vault inbox for later processing. |
data — the warehouse surface| Skill | Invoke | Description |
|---|---|---|
sql-data-analysis | /data:sql-data-analysis | Apply SQL standards for analytics, reporting, extraction, and transformation. |
data-governance | /data:data-governance | Query Snowflake's ACCOUNT_USAGE schema for masking, classification, access history, roles, and user auditing. |
Each group directory under skills/ is one Claude Code or Cursor plugin. A member's SKILL.md and any local scripts/, assets/, and references/ are installed together. Codex uses symlinks to these same source directories.
Installing a group installs all of its skills. Per-skill installation is not available; the Cursor fallback below is the only path that installs individual skills.
Use this repository URL:
https://github.com/victormacaubas/data-engineering-skills.git
In Claude Code, add the marketplace:
/plugin marketplace add https://github.com/victormacaubas/data-engineering-skills.git
Install the groups you want:
/plugin install craft@data-engineering-skills
/plugin install flow@data-engineering-skills
/plugin install data@data-engineering-skills
Skills then appear under / with their group prefix — /craft:structure-review, /flow:orchestrate, /data:sql-data-analysis. The prefix is applied by Claude Code and cannot be turned off.
Mods are Claude Code plugins that hook into a session and draw their own interface. Each mod installs on its own, independently of the skill groups:
| Mod | Description |
|---|---|
snowflake-query-panel | Shows the SQL that the main session and its subagents run through the snowflake-raw MCP server in a pane, grouped by agent. Requires the snowflake-raw MCP server. |
Install one the same way as a group:
/plugin install snowflake-query-panel@data-engineering-skills
Mods are available for Claude Code only. Cursor CLI and Codex have no equivalent, and their catalogs and installers do not list mods. The mod API is early access and may change between Claude Code releases. See docs/mods.md.
Open /plugin in Claude Code, refresh the data-engineering-skills marketplace, then update each installed group and mod plugin. Marketplace installs are snapshots rather than live symlinks to a checkout.
Clone this repository, then run the symlink-first installer:
git clone https://github.com/victormacaubas/data-engineering-skills.git
cd data-engineering-skills
./scripts/install-codex-skills.sh --skills all
Skills are linked into ~/.codex/skills/ by default and invoke unprefixed. Select individual skills or groups, or use copies where symlinks are unsuitable:
./scripts/install-codex-skills.sh --skills sql-data-analysis,data-governance
./scripts/install-codex-skills.sh --group data
CODEX_SKILLS_DIR=/custom/codex/skills ./scripts/install-codex-skills.sh --skills all
./scripts/install-codex-skills.sh --skills all --copy
Install the matching native TOML agents with the same installer used for Claude Code and Cursor CLI:
./scripts/install-agents.sh --platform codex --agents all
./scripts/install-agents.sh --platform codex --agents pathfinder,researcher
CODEX_AGENTS_DIR=/custom/codex/agents ./scripts/install-agents.sh --platform codex --agents all
Codex agent definitions live at agents/codex/*.toml and are linked into ~/.codex/agents/ by default.
Symlink installs reflect edits after git pull. If installed with --copy, rerun the same installer command after pulling. Rerun the installer after adding a new skill.
In an interactive Cursor CLI session:
/plugin marketplace add https://github.com/victormacaubas/data-engineering-skills.git
Or register the marketplace from a shell:
agent plugin marketplace add https://github.com/victormacaubas/data-engineering-skills.git
Then open /plugin in Cursor CLI to browse the registered marketplace and install the craft, flow, and data plugins. Public Cursor Marketplace submission is not required.
If Cursor CLI reports [permission_denied] Third-party plugin imports are disabled by team admin settings, ask an admin to import this repository as a Team Marketplace. When plain user skills are permitted, you can instead clone this repository and install skills directly:
./scripts/install-cursor-skills.sh --skills all
./scripts/install-cursor-skills.sh --skills architecture-baseline,sql-data-analysis
./scripts/install-cursor-skills.sh --group data
./scripts/install-cursor-skills.sh --group craft,data
--skills takes bare skill names and --group takes group names; the two are mutually exclusive. Either way the target directory is flat, so fallback skills are invoked unprefixed — structure-review, not craft:structure-review. That is the one behavioural difference from a marketplace install.
The fallback symlinks skills into ~/.cursor/skills/ by default. It preserves unselected skills and backs up conflicting non-repository paths. Use --copy for copies or override the target:
CURSOR_SKILLS_DIR=/custom/cursor/skills \
./scripts/install-cursor-skills.sh --skills all
This fallback installs plain Cursor skills, not plugins. Marketplace installation remains the preferred distribution path.
Open /plugin in Cursor CLI to refresh the marketplace and update installed groups. For fallback symlinks, git pull updates the linked skills; rerun install-cursor-skills.sh after pulling only when the fallback used --copy or when a new skill was added.
Marketplace plugins contain skills only. Install these agents separately before using an agent-dependent skill:
| Skill | Required agents |
|---|---|
/craft:architecture-baseline | researcher |
/flow:orchestrate | implementer, pathfinder, researcher |
/flow:scout | pathfinder, researcher |
For example:
./scripts/install-agents.sh --platform both --agents implementer,pathfinder,researcher
Clone the repository before running its agent installer:
git clone https://github.com/victormacaubas/data-engineering-skills.git
cd data-engineering-skills
Run the interactive agent-only wizard:
./scripts/install.sh
For non-interactive installs:
./scripts/install-agents.sh --platform claude --agents all
./scripts/install-agents.sh --platform codex --agents all
./scripts/install-agents.sh --platform cursor --agents pathfinder,researcher
./scripts/install-agents.sh --platform both --agents implementer,pathfinder,researcher
The default mode creates symlinks. Use --copy when links are unsuitable:
./scripts/install-agents.sh --platform both --agents all --copy
Claude Code agents install into ~/.claude/agents/; Codex agents install into ~/.codex/agents/; Cursor CLI agents install into ~/.cursor/agents/. Override any target with an environment variable:
CLAUDE_AGENTS_DIR=/custom/claude/agents \
./scripts/install-agents.sh --platform claude --agents all
CURSOR_AGENTS_DIR=/custom/cursor/agents \
./scripts/install-agents.sh --platform cursor --agents all
CODEX_AGENTS_DIR=/custom/codex/agents \
./scripts/install-agents.sh --platform codex --agents all
The installer backs up an existing non-repository target as <path>.bak.<timestamp>. It does not remove unselected agents.
Agent symlinks reflect edits after git pull. If agents were installed with --copy, rerun the same agent install command after pulling.
Use /plugin in Claude Code or Cursor CLI to uninstall and manage marketplace skills. For Codex, remove the confirmed skill path from ~/.codex/skills/.
For a Cursor fallback installation, remove the confirmed skill path from ~/.cursor/skills/.
To uninstall a custom agent, remove its file or symlink from the relevant target:
rm ~/.claude/agents/<agent-name>.md
rm ~/.cursor/agents/<agent-name>.md
Removing an agent does not uninstall any skill that depends on it.
Earlier releases installed skills directly under ~/.claude/skills/ and ~/.codex/skills/. No marketplace command or agent installer removes old files, symlinks, directories, or .bak.<timestamp> backups.
Install and verify the replacement first. Then inspect any legacy target before deleting it:
ls -ld ~/.claude/skills/<skill-name> ~/.codex/skills/<skill-name> 2>/dev/null
readlink ~/.claude/skills/<skill-name> 2>/dev/null || true
readlink ~/.codex/skills/<skill-name> 2>/dev/null || true
Remove only a path you have confirmed is an obsolete repository-owned link or an unneeded copy. Preserve customized copies and backups. If both a marketplace plugin and a legacy install remain, the client may discover the skill twice.
data-engineering-skills/
├── .claude-plugin/marketplace.json # Claude Code catalog: skill groups and mods
├── .cursor-plugin/marketplace.json # Cursor CLI skill catalog
├── skills/
│ └── <group>/ # craft | flow | data — one plugin each
│ ├── README.md
│ └── <skill-name>/
│ ├── SKILL.md
│ ├── scripts/
│ ├── assets/
│ └── references/
├── mods/
│ └── <mod-name>/ # Claude Code only, one plugin each
│ ├── .claude-plugin/plugin.json
│ ├── hooks/
│ └── types/
├── agents/
│ ├── claude/<agent-name>.md
│ ├── codex/<agent-name>.toml
│ └── cursor/<agent-name>.md
├── scripts/ # Agent installers and Cursor/Codex skill installers
├── docs/ # Authoring guides
└── openspec/ # Tracked changes
See docs/authoring.md to create or graduate a skill, docs/mods.md to author a Claude Code mod, and docs/agents.md to author platform-specific agents.
A marketplace is not listed
Check that the Git URL is reachable, then add it again with the marketplace-add command for that client. A recent client version with Git-backed marketplace support is required. If Cursor team policy blocks third-party imports, use the documented Team Marketplace or plain-skill fallback.
A skill is missing from a marketplace
A skill loads only when its group entry lists it. Check that the skill directory sits under skills/<group>/ and that its path appears in that group's skills array in both catalogs — a skill present on disk but absent from the array loads nowhere, silently. Work under skills/in-progress/ and skills/deprecated/ is excluded by design.
A skill appears twice
Most often an old per-skill plugin is still installed alongside its group; the two show as /structure-review:structure-review and /craft:structure-review. Uninstall the old one. Otherwise inspect ~/.claude/skills/<skill-name> and, for legacy cleanup only, ~/.codex/skills/<skill-name>, and remove only the obsolete installation after verifying whether it is a symlink, copy, or customized directory.
An agent runs without its skill
Agent skill preloads are namespaced (craft:structure-review). A preload that does not resolve is skipped silently, with only a debug-log warning, so the agent still launches and still returns output. If a review agent's output looks unlike the skill's method, check its skills: frontmatter against the current group names.
An agent-dependent skill cannot dispatch a worker
Install the required agent names for the current platform with scripts/install-agents.sh, then restart or reload the client if needed.
An installed agent does not appear
Check ~/.claude/agents/, ~/.codex/agents/, or ~/.cursor/agents/ and rerun the installer for that platform.
hooks/register.ts 47 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { GUARDED_TOOLS, IMPLEMENTER, TEST_AUTHOR, denyReason, relativeTo } from './rules'
4
5const PLUGIN = 'agent-write-guard'
6
7const targetPath = (e: Record<string, unknown>): string | undefined => {
8 const path = e.tool === 'NotebookEdit' ? e.notebook_path : e.file_path
9
10 return typeof path === 'string' ? path : undefined
11}
12
13async function agentTypeOf($: EngineInterface, agentId: string): Promise<string | undefined> {
14 try {
15 const listed = (await $.agent.list()).find(agent => agent.id === agentId)
16 if (listed === undefined) {
17 $.ui.log(`${PLUGIN}: agent ${agentId} is not in the session's agent list; letting the call through`, {
18 to: 'debug',
19 })
20 }
21
22 return listed?.type
23 } catch (error) {
24 const reason = error instanceof Error ? error.message : String(error)
25 $.ui.log(`${PLUGIN}: agent lookup failed: ${reason}; letting the call through`, { to: 'debug' })
26
27 return undefined
28 }
29}
30
31export const register: Register = on => {
32 on('tool.call', { tool: GUARDED_TOOLS }, async ($, e, next) => {
33 if (e.agentId === undefined) {
34 return next(e)
35 }
36 const agentType = await agentTypeOf($, e.agentId)
37 if (agentType !== IMPLEMENTER && agentType !== TEST_AUTHOR) {
38 return next(e)
39 }
40 const path = targetPath(e as Record<string, unknown>)
41 const relative = path === undefined ? undefined : relativeTo(await $.session.cwd(), path)
42 const reason = relative === undefined ? undefined : denyReason(agentType, relative)
43
44 return reason === undefined ? next(e) : { deny: reason }
45 }).catch(($, e, next) => next(e))
46}
47hooks/rules.ts 96 lines1export const IMPLEMENTER = 'implementer'
2export const TEST_AUTHOR = 'test-author'
3
4export const GUARDED_TOOLS = /^(Edit|Write|NotebookEdit)$/
5
6export const IMPLEMENTER_REASON =
7 'Test files belong to test-author. If this test looks wrong, list it under Disputed tests.'
8export const TEST_AUTHOR_REASON =
9 'Production code belongs to the implementer. Report the change under Needed production changes instead.'
10
11const TEST_TREE = [
12 '**/tests/**',
13 '**/__tests__/**',
14 '**/conftest.py',
15 '**/test_*.py',
16 '**/*_test.py',
17 '**/*.test.ts',
18 '**/*.spec.ts',
19]
20
21// Paths test-author may write besides the test tree: its report directory and test-runner config.
22const TEST_AUTHOR_EXTRAS = [
23 '.orchestrate/**',
24 '**/pytest.ini',
25 '**/pyproject.toml',
26 '**/setup.cfg',
27 '**/tox.ini',
28 '**/jest.config.*',
29 '**/vitest.config.*',
30 '**/playwright.config.*',
31]
32
33const escapeRegex = (text: string): string => text.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
34
35// Covers only what the lists above use: `**` as a whole segment, `*` inside a segment, literals.
36export function globToRegex(glob: string): RegExp {
37 const segments = glob.split('/')
38 const parts = segments.map((segment, index) => {
39 if (segment === '**') {
40 return index === segments.length - 1 ? '.*' : '(?:[^/]+/)*'
41 }
42 const body = segment.split('*').map(escapeRegex).join('[^/]*')
43
44 return index === segments.length - 1 ? body : `${body}/`
45 })
46
47 return new RegExp(`^${parts.join('')}$`)
48}
49
50const TEST_TREE_REGEXES = TEST_TREE.map(globToRegex)
51const TEST_AUTHOR_EXTRA_REGEXES = TEST_AUTHOR_EXTRAS.map(globToRegex)
52
53const matchesAny = (regexes: RegExp[], path: string): boolean => regexes.some(regex => regex.test(path))
54
55export const isTestTree = (relativePath: string): boolean => matchesAny(TEST_TREE_REGEXES, relativePath)
56
57function normalize(path: string): string[] {
58 const out: string[] = []
59 for (const segment of path.split('/')) {
60 if (segment === '' || segment === '.') {
61 continue
62 }
63 if (segment === '..') {
64 out.pop()
65 } else {
66 out.push(segment)
67 }
68 }
69
70 return out
71}
72
73/** The path relative to `cwd`, or undefined when it resolves outside it. */
74export function relativeTo(cwd: string, path: string): string | undefined {
75 const base = normalize(cwd)
76 const target = normalize(path.startsWith('/') ? path : `${cwd}/${path}`)
77 if (target.length <= base.length || base.some((segment, index) => target[index] !== segment)) {
78 return undefined
79 }
80
81 return target.slice(base.length).join('/')
82}
83
84/** The reason to deny `agentType` writing `relativePath`, or undefined to let the call through. */
85export function denyReason(agentType: string, relativePath: string): string | undefined {
86 const inTestTree = isTestTree(relativePath)
87 if (agentType === IMPLEMENTER && inTestTree) {
88 return IMPLEMENTER_REASON
89 }
90 if (agentType === TEST_AUTHOR && !inTestTree && !matchesAny(TEST_AUTHOR_EXTRA_REGEXES, relativePath)) {
91 return TEST_AUTHOR_REASON
92 }
93
94 return undefined
95}
96