DEV Community

Cover image for I automated away an hour of project setup with a Claude Code skill
Digital Craft Workshop
Digital Craft Workshop

Posted on • Originally published at Medium

I automated away an hour of project setup with a Claude Code skill

Not a member? Use this link.

Every new SaaS project I start used to eat an hour of clicking — GitHub repo, npx create-next-app, copy-paste the DB config from my last project, walk through the Render dashboard, paste env vars, wait for the first deploy to fail because I forgot node server.js.

I did this three times in a row for three sibling projects. Then I automated the whole thing into a 90-second command.

The setup tax

I keep a folder called foundary-tools where each subdirectory is a separate SaaS or internal tool. Three projects in, every new sibling shares 90% of the same scaffolding.

Same Next.js 16 + React 19 skeleton. Same Drizzle + postgres-js setup. Same Tailwind v4 config. Same server.js that binds IPv4 explicitly because Render's port scanner doesn't see IPv6-only processes. Same .gitignore. Same tsconfig.json. Even the background job architecture is shared.

The only thing that changes between projects is the domain logic — about 5% of the initial commit.

So the other 95% was me, manually, every single time:

  • gh repo create danielrusnok/<name> --private
  • Copy a Next.js skeleton from the last project
  • Rename "name" in package.json, update CLAUDE.md, update the metadata in layout.tsx
  • Wire Drizzle against the shared Postgres, pick a unique pgSchema name
  • Paste the specific server.js with the 0.0.0.0 bind trick
  • Open dashboard.render.com, click New → Web Service, connect repo, fill in build/start commands, paste DATABASE_URL
  • Commit, push, wait 3 minutes, watch the first deploy fail because I typed something wrong
  • Fix, push again, wait another 3 minutes

An hour, easily. And at least twice I deployed a project that was silently missing an env var because I was tired and skipped a step.

How Claude Code skills work

If you use Claude Code, a "skill" is a procedural playbook Claude loads from **~/.claude/skills/**. Each skill is a directory with a SKILL.md — frontmatter describing when to trigger (natural-language phrases), plus step-by-step instructions for Claude to follow.

Skills can include helper scripts. You delegate judgment to the LLM and deterministic work to scripts. I leaned into this split even harder when I added persistent memory to Claude Code — same principle, different tool.

The division I landed on:

  • Claude handles: asking the user for inputs, confirming destructive actions, interpreting errors from gh / npm / the Render API, deciding whether to retry or escalate.
  • Python handles: walking a template directory, substituting placeholders, reading env vars from sibling projects, POSTing to Render's REST API.

Don't make the LLM do regex on **package.json**. Don't make the script decide whether to retry a failed network call. Each does what it's good at.

Nine steps, three approval gates

I named the skill foundary-new-project. It triggers on phrases like "nový foundary projekt" or "new foundary project" — same natural-language entry point as every other skill I use.

The workflow is nine steps, three of which require my explicit approval:

  1. Validate & read secrets from sibling .env files
  2. Scaffold the template into ~/foundary-tools/<name>/
  3. Run npm install
  4. Smoke-build with npm run build — fail fast before anything external happens
  5. git init + first commit
  6. Approval gate: create GitHub repo, push
  7. Fetch Render owner ID (cached after the first run)
  8. Approval gate: create Render web-service via API
  9. Update CLAUDE.md with the Render service ID and print a recap

The approval gates exist because my intent changes between runs. Sometimes I want to scaffold locally and push later. Sometimes the name I picked collides with an existing Render service. A hard confirmation before external state changes is cheap insurance.


The scaffold script

scaffold.py is the least interesting part, which is how it should be:

def copy_template(target, name, schema, description):
    for src_root, dirs, files in os.walk(TEMPLATE_DIR):
        src_root_p = Path(src_root)
        rel = src_root_p.relative_to(TEMPLATE_DIR)
        dst_root = target / rel
        dst_root.mkdir(parents=True, exist_ok=True)
        for fname in files:
            src = src_root_p / fname
            dst = dst_root / fname
            raw = src.read_bytes()
            try:
                text = raw.decode("utf-8")
            except UnicodeDecodeError:
                dst.write_bytes(raw)
                continue
            dst.write_text(substitute(text, name, schema, description))
Enter fullscreen mode Exit fullscreen mode

Three placeholders: __NAME____SCHEMA____DESCRIPTION__The schema is the project name with hyphens swapped for underscores (SQL identifier rules).

I don't have a keychain integration. I don't maintain a separate secrets file. I read **DATABASE_URL** from **~/foundary-tools/drippery/.env.production** and RENDER_API_TOKEN from ~/foundary-tools/framelock/.env.local.

Why? Because those projects are already configured. The files exist. Permissions are already set to 0600. My adjacent project is my secret store by construction.

The new project's .env.local gets written with chmod 600 and added to a .gitignore that blocks .env*Normal hygiene.


The Render API call

Render has a clean v1 REST API where POST /v1/services creates a web-service with the config baked in.

create_body = {
    "type": "web_service",
    "name": args.name,
    "ownerId": args.owner_id,
    "repo": args.repo,
    "autoDeploy": "yes",
    "branch": "main",
    "serviceDetails": {
        "plan": "free",
        "region": "oregon",
        "env": "node",
        "envSpecificDetails": {
            "buildCommand": "npm ci && npm run build",
            "startCommand": "node server.js",
        },
    },
    "envVars": [
        {"key": "DATABASE_URL", "value": args.database_url},
    ],
}
Enter fullscreen mode Exit fullscreen mode

The owner ID is cached after the first run in ~/.claude/skills/foundary-new-project/.render-owner-id, so subsequent scaffolds skip a round-trip.

The whole script is about 80 lines of urllib.request — no SDK, no extra dependency. If the POST returns 409 or 400, Python exits with the error text and Claude reads it, tells me, and asks what to do.

That loop — script fails loud, LLM interprets, human decides — is why I reach for skills instead of **set -e** bash scripts.

If you'd rather get this kind of solo-builder setup broken into a few short, copy-paste steps in your inbox, I turned the essentials into The Claude Code Memory Starter — a free email series.


Template, not generator

The template/ directory is a real Next.js skeleton I can open and edit. When the stack evolves — new Next.js version, Tailwind upgrade, a server.js tweak — I edit the template files directly and the next scaffold picks up the change.

No code generator to maintain. No AST rewrites. Just files with three placeholders.

The template has 15 files: app/{layout,page,globals}lib/db/{index,schema}lib/env.tsserver.jsdrizzle.config.tsnext.config.tspostcss.config.mjstsconfig.json.gitignore.env.exampleAGENTS.mdCLAUDE.mdREADME.mdpackage.jsonEvery one is either identical across projects or differs only by name/description.

The landing page is deliberately dumb — a hero with the project name and a "Coming soon" line. No DB calls, no auth, nothing that could break the first deploy. The first deploy's only job is to prove the pipeline works. I push every change straight to production anyway — this just makes sure the wiring is correct before real code lands.

90 seconds, zero dashboard clicks

From "I want a new project called X" to "a private GitHub repo exists, a free Render service is deploying from main, and npm run dev works locally" — about 90 seconds of wall time. Most of that is npm install and the initial next build.

Three approvals. Zero clicks in the Render dashboard. Zero copy-paste from old projects. Zero chance I forgot to rename something in package.json.

Compared to an hour of manual work where I screwed up half the steps — worth it.


Three things worth stealing

Skills beat shell scripts for anything with branching logic. A bash script with set -e exits on any failure. An LLM-driven skill reads the error, decides whether to retry, rephrase, or ask the human. The difference shows up the first time gh auth status is expired or the Render API rate-limits you — the bash script dies, the skill adapts.

Approval gates before external state changes. Local filesystem mutations and git commit are cheap to undo. GitHub repo creation, Render API calls, git push — those leak state outside my machine. A hard [y/N] confirmation before each one is the right trade-off.

Read secrets from adjacent projects. I was about to overthink secret storage — keychain, 1Password CLI, encrypted env file. My sibling projects' .env.local files are already provisioned and protected. One less system to maintain.


What's next

The skill is globally installed at ~/.claude/skills/foundary-new-project/, so it triggers in any Claude Code session regardless of working directory. The scripts hard-code paths into ~/foundary-tools/, which keeps the scope tight.

Next thing I'm automating: the shutdown flow. When a project turns out to be a dead end, I want "archive the repo, delete the Render service, drop the pgSchema, move the folder to ~/foundary-tools/.graveyard/" as one command. Same skill pattern, reversed.

If you run a portfolio of similar side projects as a solo builder, this is the best automation I've set up this year. The real win is the marginal cost: I can spin up a new experiment at 9:47 PM on a Tuesday and have it deploying by 9:49.


External Sources

If you want more patterns like this landing in your inbox, The Claude Code Memory Starter walks you through the setup over a few short emails — free.

Top comments (0)