SLOPSHOPPER

agent-write-guard

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.

newguard
A shopper browsing a rack in a slop shop
README

<img src="assets/personal-logo.png" alt="Personal logo" width="160">

data-engineering-skills

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

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

SkillInvokeDescription
architecture-baseline/craft:architecture-baselineDecide a new project's architectural constraints before feature work. Requires the separately installed researcher agent.
python-engineering-standards/craft:python-engineering-standardsApply production Python standards for layout, typing, configuration, logging, error handling, testing, and packaging.
code-audit/craft:code-auditAudit code in any language and write a machine-readable report covering correctness, security, performance, architecture, error handling, and readability.
structure-review/craft:structure-reviewReview the shape and project conformance of a finished change before merge or archive.

flow — how work gets driven from idea to build

SkillInvokeDescription
scout/flow:scoutExplore work whose shape is not settled and return a decision-ready briefing. Requires the separately installed pathfinder and researcher agents.
grill-me/flow:grill-mePressure-test ideas and change artifacts before implementation.
orchestrate/flow:orchestrateDrive workers to implement a bounded plan. Requires the separately installed implementer, pathfinder, and researcher agents.
write-ticket/flow:write-ticketWrite Jira tickets and comments in plain language through the Atlassian MCP.
stash/flow:stashPark raw content in an Obsidian vault inbox for later processing.

data — the warehouse surface

SkillInvokeDescription
sql-data-analysis/data:sql-data-analysisApply SQL standards for analytics, reporting, extraction, and transformation.
data-governance/data:data-governanceQuery 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.

Install

Use this repository URL:

https://github.com/victormacaubas/data-engineering-skills.git

Claude Code

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

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:

ModDescription
snowflake-query-panelShows 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.

Update

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.

Codex

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.

Update

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.

Cursor CLI

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.

Restricted-team fallback

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.

Update

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.

Skills that need custom agents

Marketplace plugins contain skills only. Install these agents separately before using an agent-dependent skill:

SkillRequired agents
/craft:architecture-baselineresearcher
/flow:orchestrateimplementer, pathfinder, researcher
/flow:scoutpathfinder, researcher

For example:

./scripts/install-agents.sh --platform both --agents implementer,pathfinder,researcher

Custom agents for Claude Code, Codex, and Cursor CLI

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.

Uninstalling

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.

Migrating legacy skill installs

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.

Repository structure

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.

Troubleshooting

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.

Source 2 files
hooks/register.ts 47 lines
1import 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}
47
hooks/rules.ts 96 lines
1export 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