add commit, penpot-login, and clean-house skills

Track the project-local Claude Code skills that were living untracked in
.claude/skills/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-06-03 09:31:54 +02:00
co-authored by Claude Opus 4.8
parent 931096cc17
commit f90bcbf5f3
5 changed files with 777 additions and 0 deletions
@@ -0,0 +1,5 @@
{
"version": "1.5.0",
"hash": "sha256:8090b99552c828ce019aa3740d8a0acb4768a443704b4e935efa23b32e91128d",
"installed_at": "2026-06-02T08:46:25Z"
}
+229
View File
@@ -0,0 +1,229 @@
---
name: clean-house
description: >
Run a documentation-discipline pass over a DQR (Decisions / Questions /
Rejected) markdown system. Audits decisions/, surfaces drift (broken
anchor links, missing Q→D backlinks, sunset-shaped phrases without
linked tickets, files over the split threshold, dead cross-references),
batches findings by category, and uses AskUserQuestion to apply fixes
interactively. Trigger this skill whenever the user asks to clean,
tidy, audit, sweep, garden, or review the state of decisions/, the
DQR system, the questions index, or the records under decisions/.
Also trigger before a release, after a session that touched many
records, or whenever the user mentions documentation drift, stale
plans, or "house" / "clean-house" / "house cleaning". Imperative —
apply fixes, not just report.
---
# clean-house
A periodic gardening pass over `decisions/`. The DQR system is
markdown-as-source-of-truth; `.pql/pql.db` is a derived index. This skill
operates on the markdown files, batches drift by category, and uses
`AskUserQuestion` to decide what to apply.
The skill is **imperative**. It really cleans. The verb-noun match is
load-bearing — a `clean-house` that only produced a report would be
misnamed. Mechanical fixes get a single batched approval, judgment-call
findings get individual prompts, and a "stop asking" escape hatch is
always present.
You — Claude — are the runtime. Read this file, read
`references/rules.md`, follow the procedure below, call `pql` and `Edit`
to do the work, call `AskUserQuestion` for the prompts.
## When to run
- Before a release.
- After a session that touched many D / Q / R records.
- When something feels stale and a sweep is wanted.
- When `pql decisions validate` is green but `decisions/` still feels off.
This skill is **not** a replacement for `pql decisions validate`.
Validate is fast, hot, and gates pre-push. clean-house is slow, cold,
on-demand. Escalation path: rules that fire often here are candidates
for promotion to `pql decisions validate`; rules that almost never fire
stay here where false positives don't slow anyone down.
## Procedure
1. **Gate on validate.** Run `pql decisions validate`. If it exits
non-zero, stop and tell the user to fix validator findings first.
Do not proceed.
2. **Load the rule catalog.** Read `references/rules.md`. Each rule
names its detection, fix, and finding-id format.
3. **Probe conventions.** Read `decisions/.clean-house.yaml` if it
exists; otherwise infer from the project. Conventions to resolve:
- `heading_level`: 2 or 3 — the depth records use (`## D-N` vs
`### D-N`). Probe by sampling the first 3 records' file
locations and looking for the first matching ATX heading.
- `backlink_phrasing`: the project's Q→D backlink phrase (default
`Resolved → D-N`; some projects use `Resolved as D-N` or
`→ D-N`).
- `file_threshold`: default 350 (RULE-FILE-OVER-THRESHOLD).
- `stale_open_q_days`: default 60 (RULE-STALE-OPEN-Q).
Pass the resolved conventions into each rule's detection. Cache
the probe result for the run.
`decisions/.clean-house.yaml` shape (all keys optional):
```yaml
heading_level: 3
backlink_phrasing: "Resolved → "
file_threshold: 500
stale_open_q_days: 90
exclude_paths: ["legacy/", "drafts/"]
```
4. **Sync the index.** Run `pql decisions sync` so the DB reflects
the markdown.
5. **Walk decisions/.** Use `pql decisions list` to enumerate
records, plus `pql decisions read <id>` per record when a rule
needs the body. For file-level rules (size, sort), read the
markdown files directly. **Filter out** any record whose
`file_path` begins with `legacy/` (or any prefix in
`exclude_paths` from the probe).
6. **Run each rule's detection.** Collect findings. Tag each with
its rule ID, finding ID (per the rule's format), category
(`mechanical` | `judgment`), and the record(s)/file(s) involved.
7. **Group findings by rule** and prompt. Honor any "skipped 3+
times" findings from the skip ledger by escalating their prompt
("skipped 3x — promote to manual decision, downgrade to
summary-only, or keep asking?"):
- **Mechanical rules** — one prompt per rule, batched. Show the
count and offer: apply all / show diff first / skip this rule /
**stop asking** (global — see below).
- **Judgment rules** — one prompt per finding. Options match the
rule's `Fix` section, plus the same global **stop asking**.
**"Stop asking" semantics (global).** When the user picks "stop
asking" on any prompt, immediately halt all further prompts in
the run — mechanical batches not yet asked AND remaining
judgment findings. Move all unprompted findings to the skip
ledger marked `deferred-stop-asking`. Skip to step 9.
**"Show diff first" loop.** When the user picks "show diff
first" on a mechanical batch, emit a unified diff of the staged
edits (one block per file), then re-prompt the same question
with the same options minus "show diff first" — preventing
infinite loops while still letting the user inspect before
committing.
8. **Apply approvals.** For mechanical rules use `Edit` to mutate
the markdown directly. For judgment rules, follow the option the
user picked (file a ticket via `pql ticket new`, mark superseded
by editing the record, etc.).
9. **Update the skip ledger and history.** Both files live at
`decisions/.clean-house-state.md`, gitignored. The file uses
per-run section headers so it doubles as run history:
```markdown
# clean-house run history
## 2026-05-06T13:42Z
fired: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1)
applied: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1)
skipped:
- RULE-ANCHOR-DRIFT D-8:#q-1-markdown-mirror-for-tickets
## 2026-05-04T11:10Z
fired: ...
```
Each section starts with a UTC ISO-8601 timestamp. `fired`
counts findings detected; `applied` counts findings the user
acted on; `skipped` lists `<rule-id> <finding-id>` pairs.
**Promotion-candidate computation.** Scan the most recent N=7
sections (or all sections if fewer). A rule is a promotion
candidate if it `fired` in ≥ 5 of those sections. Surface the
candidates in the summary; do not auto-promote.
10. **Emit the summary** (see Output below).
## Question phrasing
Always include the rule ID in the prompt so the user learns what's
being checked.
**Mechanical (batched):**
> RULE-ANCHOR-DRIFT: 12 cross-reference anchors point to headings that
> no longer exist (renamed or deleted). Apply all 12 fixes? Options:
> apply all / show diff first / skip this rule / stop asking, summarize
> the rest.
**Judgment (individual):**
> RULE-SUNSET-WITHOUT-TICKET: D-59 mentions "must track dugite-native
> releases for security updates" but has no linked T. Options: file T
> now / draft a T description first / mark as already covered (add note
> to D) / skip / stop asking.
Phrasing rules:
- **Always include "stop asking, summarize the rest."** Sessions
without bandwidth for full review need an escape. Forcing answers
to every question kills the tool.
- **Always include "skip."** Skipped findings go to the ledger; three
consecutive skips of the same finding promote the next prompt.
- **Mechanical batches; judgment doesn't.** "Apply all 12 fixes?" is
one decision against a clear delta. Twelve individual prompts defeat
the point. Conversely, judgment findings are each separate work —
presenting them as a batch hides cost.
## What this skill does NOT do
- Does not file tickets without asking.
- Does not rewrite D-record body prose, only metadata fields and
cross-reference links.
- Does not touch records under `legacy/`.
- Does not run `pql decisions validate` *as a fix* — only as a
precondition gate.
- Does not promote rules to `pql decisions validate` automatically.
Promotion is a human decision; this skill only flags candidates
("RULE-X has fired in 5 of the last 7 runs — consider promoting").
## Non-interactive context
If invoked without an interactive `AskUserQuestion` surface (a CI run,
a batch script), refuse to apply judgment fixes. Apply mechanical
fixes only if the user explicitly says so via the trigger phrase
("auto-apply mechanical, skip judgment"); otherwise emit the summary
and exit without mutating files.
## Output
At the end, emit a summary block:
```
clean-house — sweep complete
────────────────────────────
Files scanned: 11
Records parsed: 62 D, 23 Q, 11 R
Conventions: heading_level=3 backlink="Resolved → "
Mechanical fixes: 14 applied, 0 deferred
Judgment findings: 3 acted on, 2 deferred (in state file)
Promotion candidates: RULE-ANCHOR-DRIFT (5/7 runs)
Touched files: decisions/architecture.md, decisions/questions.md
```
If this is the first run (no prior history) or there are fewer
than 5 prior runs, `Promotion candidates:` reports
`(none — N/7 runs of history)` instead of decorative empty content.
The summary names the touched files so the next commit message can be
honest about what changed. The skill does not commit on its own.
## Versioning
clean-house ships embedded in the pql binary; its version is the
pql version (`pql --version`). To see what changed and when, read
`CHANGELOG.md` or `git log internal/skill/clean-house/` in the pql
repo. Keep this file and `references/rules.md` consistent with
each other when iterating — they're the contract.
@@ -0,0 +1,386 @@
# clean-house rule catalog
The rules clean-house runs and the reasoning behind each. New rules
land here as their own entry. Catalog churn — additions, refinements,
retirements — is tracked in pql's `CHANGELOG.md` and `git log
internal/skill/clean-house/`. When a rule is retired, leave a
**Retired** stub (ID + one-line reason + retirement commit) so the
trail of "we used to check X" is recoverable from this file alone.
## Reading a rule
Each entry below has:
- **ID** — Stable identifier (e.g. `RULE-ANCHOR-DRIFT`). Used in
AskUserQuestion prompts and skip-ledger entries so the user learns
what's being checked.
- **Category** — `mechanical` or `judgment`. Drives whether findings
batch into one prompt or each get their own.
- **Finding ID** — Format string for the per-finding stable
identifier. Skip-ledger entries are written as `<rule-id>
<finding-id> <ISO-date>`; subsequent runs use the same format to
recognize "same finding skipped 3 runs in a row" and escalate.
Use only stable inputs (record IDs, file paths, slug strings, body
hashes) — never line numbers or timestamps.
- **Detection** — Concrete steps to find violations. Names the
pql command or file primitive used.
- **Fix** — Mechanical: deterministic action. Judgment: the prompt
option list and what each option does.
- **Why** — One paragraph on the failure mode this rule guards
against. Future-you reading the rule benefits from the reasoning,
not just the check.
---
## RULE-ANCHOR-DRIFT
**Category:** mechanical
**Finding ID:** `<source-record>:<link-target>` (e.g.
`D-8:#q-1-markdown-mirror-for-tickets` or
`D-8:questions.md#q-1`).
**Detection:**
Anchor-only markdown links (`[text](#slug)`) resolve against the
**source file's full heading set**, not the body of a single record
— record-level headings (`### D-N: …`) live as siblings in the
file and are valid link targets. The previous detection (which used
`pql decisions read`'s body-only `headings` array) missed these
and produced false positives.
Per source body:
1. Open the source body's file (`<vault>/<file_path>` from
`pql decisions list`) and extract every ATX heading. Build a
slug index for the file using the GFM convention (lowercase,
hyphenate spaces, drop punctuation, disambiguate duplicates with
`-1`/`-2`). Cache per file — every record in that file uses the
same index.
2. For each `[text](target)` link in the body:
- Anchor-only (`#slug`): check `slug` against the **source file's**
index. Missing → flag.
- Cross-file (`path.md#slug`): resolve `path.md` relative to the
source file's directory; check `slug` against that file's
index. Missing file or missing slug → flag.
**Fix:**
If the slug exists in a sibling file's index (same directory) and
the link is anchor-only, rewrite to `path.md#slug` (the canonical
cross-file form). If the slug exists in the source file's index
with edit-distance ≤ 2 from the link target, rewrite to the
matched slug. Otherwise downgrade to judgment ("no auto-fix; the
heading was removed, not renamed") with options: drop the link /
point elsewhere / mark as intentionally dangling.
**Fix:**
If exactly one heading with a closely-matching slug exists (slug
edit-distance ≤ 2 or substring match), rewrite the link to point at
that heading. If no plausible match exists, downgrade the finding to
judgment ("no auto-fix; the heading was removed, not renamed") and
prompt the user with options: drop the link / point elsewhere / mark
as intentionally dangling.
**Why:**
When a heading is renamed for clarity, every cross-reference pointing
at the old slug silently breaks. The rendered docs still look fine —
the link just goes nowhere. Without periodic sweeps these decay
indefinitely; the cost of the sweep is small compared to the cost of
a reader following a dead link and losing trust in the index.
---
## RULE-MISSING-Q-BACKLINK
**Category:** mechanical
**Finding ID:** `<d-id>:<q-id>` (e.g. `D-7:Q-2`). Order is always
D-first regardless of which side is missing the backlink.
**Detection:**
For each D record body, find lines of the shape `Resolves: Q-N`
(plain text, typically in the metadata block at the top of the
record). For each Q-N referenced, fetch that Q record's body via
`pql decisions read Q-N` and check whether it contains a line of the
shape `Resolved → D-N` (or equivalent backlink phrasing — match the
project's own convention; default pattern is `Resolved → D-N`).
Also check the reverse direction: any Q record claiming
`Resolved → D-N` whose D-N body lacks `Resolves: Q-N`.
**Fix:**
Insert the missing backlink in-place using `Edit`:
- Missing on the D side: add `Resolves: Q-N` to the D's metadata
block (typically right after `Domain:` / `Status:` lines).
- Missing on the Q side: append `Resolved → D-N` to the Q's status
line (or in the conventional position for that project).
**Why:**
Bidirectional Q↔D links are the navigation backbone of the DQR
system. When one side drifts, search-by-decision finds nothing for
that question and search-by-question doesn't surface its resolution.
The asymmetric state usually arises from a hand-edit on one record
that forgot to update the other; the fix is purely mechanical because
the correct content is already determined by the existing pointer
in the other direction.
---
## RULE-RECORD-SORT
**Category:** mechanical
**Finding ID:** `<file-path>` (e.g. `decisions/architecture.md`).
The whole file is one finding — sort applies to the file as a
unit.
**Detection:**
For each `decisions/*.md` file, read it directly (no pql) and find
all top-level `## D-N` / `## Q-N` / `## R-N` headings in order. Strip
the prefix, parse the numeric suffix, and check whether the sequence
is monotonically ascending within each ID family (D, Q, R kept
separate — files commonly mix families).
Only flag a file if it is **otherwise tidy** — currently no out-of-
order amendments interleaved with new records. The heuristic: if the
sort would touch fewer than three records out of position, apply it;
if more, downgrade to judgment ("this file looks deliberately
arranged — confirm before reordering"). Three is a soft threshold; if
the file's last commit message contains `WIP` or `do-not-sort`, skip.
**Fix:**
Reorder the records within the file so each ID family is ascending.
Preserve everything else (headings between record blocks, any prose
prelude/postlude). Use `Edit` with full block replacement, not in-
place line shuffling — easier to verify the diff.
**Why:**
Records added at the bottom of a file are easy to write but hard to
find. Sorted IDs let a reader find D-37 by jumping to the
two-thirds mark of the file rather than scanning. The cost of the
sort is one reordering pass; the benefit is paid back on every
subsequent read.
---
## RULE-EOF-NORMALIZATION
**Category:** mechanical
**Finding ID:** `<file-path>` (one finding per file).
**Detection:**
For each `decisions/*.md` file (and any other markdown the skill
touched during this run), read directly. Flag if:
- File does not end with exactly one `\n`.
- Any line contains trailing whitespace before its `\n`.
**Fix:**
Trim trailing whitespace from each line. Ensure exactly one trailing
newline at end of file. Use `Edit` only if a violation was found —
this rule must not produce a no-op diff.
**Why:**
Editor and git config drift causes whitespace creep that's invisible
in rendering but pollutes diffs (every record edit ends up touching
unrelated lines). A periodic normalization keeps future diffs clean
without forcing per-editor enforcement on every contributor.
---
## RULE-SUNSET-WITHOUT-TICKET
**Category:** judgment
**Finding ID:** `<record-id>:<phrase-hash-8>` where phrase-hash-8 is
the first 8 hex chars of `sha256(<matched-phrase>)`. Lets the
ledger distinguish two sunset phrases in the same record.
**Detection:**
For each D record body (via `pql decisions read <id>`), grep for
sunset-shaped phrases. Default regex set:
```
(?i)\b(delete|remove|sunset|kill[ -]?switch|tear[ -]?down) when\b
(?i)\bmust (track|monitor|watch|follow)\b
(?i)\brevisit (when|after|once)\b
(?i)\b(deprecate|retire) (when|after|once)\b
```
For each match, run `pql ticket list --decision <id>`. If no tickets
exist, the D has work-shaped intent without a tracked T — flag.
**Fix (prompt options):**
- **File T now** — Run `pql ticket new task "<phrase>" --decision <id>`,
then prompt the user for a description (or call `pql ticket refine
write <T> --description ...` after creation).
- **Draft a T description first** — Open an `AskUserQuestion` for the
description, then file as above.
- **Mark as already covered** — Add a note to the D body
("Tracked under T-N") via `Edit`. Skill does not assert which T;
user provides the ID.
- **Skip** — Adds to the ledger.
- **Stop asking** — Skip remaining sunset findings, summarize.
**Why:**
Sunset-shaped intent ("we'll handle X when Y happens") is the most
common source of accumulated debt in a DQR system: the trigger
condition arrives, nobody remembers the D, the work doesn't happen.
Linking each sunset to a T is the simplest defense — a T is a
backlog item that surfaces in `plan whatsnext` / `plan board`. The
fix is judgment because the right action depends on whether the D's
condition is still relevant, whether it's already covered, and what
the right scope is for the resulting T.
---
## RULE-FILE-OVER-THRESHOLD
**Category:** judgment
**Finding ID:** `<file-path>` (one finding per file; threshold
choice is judgment, not a per-line problem).
**Detection:**
For each `decisions/*.md` file, count lines (via `wc -l` or
equivalent). Default threshold: **350 lines**. Configurable per
project — read `decisions/.clean-house.yaml` if present, key
`file_threshold`. Fall back to 350.
**Fix (prompt options):**
- **Split now (which axis?)** — Prompt for the split axis: by ID
family (D/Q/R), by domain, by date range, custom. Then perform the
split: create new file(s), move records, update any anchor links
pointing into the moved records (run RULE-ANCHOR-DRIFT in apply
mode against the affected files after the split).
- **Accept and raise the threshold** — Update
`decisions/.clean-house.yaml`'s `file_threshold` to the next
reasonable round number above the current line count.
- **Defer** — Skip until next run.
**Why:**
A single decisions file growing past ~350 lines is the point at which
linear scanning starts to lose to grep, and where the cost of
splitting (renaming anchors) is still small. Beyond ~600 lines the
split cost compounds. The threshold is judgment because some projects
deliberately keep one file per domain and accept the size; others
split aggressively. The skill surfaces the question; it doesn't
decide.
---
## RULE-DEAD-FILE-REFERENCE
**Category:** judgment
**Finding ID:** `<record-id>:<path-token>` (path token as written
in the body, not resolved).
**Detection:**
For each D record body (via `pql decisions read <id>`), grep for
path-shaped tokens. Default regex:
```
\b([\w./-]+\.(md|go|py|sql|yaml|yml|toml))\b
```
The first detection pass produced 6/6 false positives in
real-world use; the regex is necessary but not sufficient. For
each match, apply the filters below in order — if any matches,
**skip without flagging**:
1. **Placeholder filter.** Token contains `T-NNN`, `D-NNN`,
`Q-NNN`, `R-NNN`, `...`, `<`, `>`, or `*` — it's a pattern,
not a real path.
2. **Source-relative resolution.** Resolve the token against the
source file's directory (`<vault>/<file_path>`'s dir). If
`[ -e <resolved> ]`, the reference is live.
3. **Repo-relative resolution.** If `[ -e <token> ]` from the
repo root, the reference is live.
4. **Basename-fallback.** Run `find <repo-root> -name <basename>`
(where basename is the last path component). If exactly one
match exists, treat the reference as live and emit a low-
priority "consider rewriting to the absolute path" note (not a
judgment finding). If multiple matches exist, do not flag —
the token is too ambiguous.
Only after all four filters miss does the reference qualify as
truly dead and warrant a judgment prompt.
**Fix (prompt options):**
- **Update reference** — Prompt for the new path; rewrite the
reference via `Edit`. If the user types a path, validate it exists
before applying.
- **Mark superseded** — Add a note to the D ("This decision
references files no longer present; the underlying constraint
was retired by D-N") and prompt for the superseding D ID.
- **Defer** — Skip until next run.
**Why:**
Decisions reference code paths to ground their reasoning in the
codebase that motivated them. When the code moves or gets deleted,
the D's reasoning becomes harder to verify. Dead refs are a signal
that either the decision should be updated or the decision itself is
no longer load-bearing — both are judgment calls the user needs to
make.
---
## RULE-STALE-OPEN-Q
**Category:** judgment
**Finding ID:** `<q-id>` (one finding per Q-record; staleness
threshold is global per run, so a Q is either stale or not).
**Detection:**
Run `pql decisions list --type question --status open`. For each
returned record, check the `date` field. Default staleness threshold:
**60 days** (configurable via `decisions/.clean-house.yaml` key
`stale_open_q_days`). Flag any Q with `date` older than the
threshold.
**Fix (prompt options):**
- **Still relevant** — Touch the Q's `date` field to today, optionally
add a one-line "still open as of YYYY-MM-DD: <reason>" to the body.
- **Nudge to load-bearing index** — If the project keeps a
load-bearing Qs list (a curated index of unresolved questions
affecting current work), prompt for inclusion and add the Q there.
- **Mark withdrawn** — Change the Q's status to `withdrawn` (the DQR
system's terminal state for "no longer worth answering"); the
user supplies a one-line reason.
- **Defer** — Skip until next run.
**Why:**
Open questions are useful when they're current; stale ones become
ambient noise that drowns the signal of new questions. Most
projects don't enforce explicit Q-lifecycle, so without a periodic
prompt the open list grows forever. The right action depends on
whether the question is still open in fact — not just in metadata —
which only the user can confirm.
+53
View File
@@ -0,0 +1,53 @@
---
name: commit
description: Create a well-formatted git commit with staged changes
---
# Git Commit
Create a well-formatted commit with staged changes following best practices.
## Steps
1. Run `git status --porcelain` to check for changes
2. If no staged changes, show unstaged files and ask what to stage
3. Run `git diff --cached` to review staged changes
4. Generate a commit message following Conventional Commits format:
- `feat:` new feature
- `fix:` bug fix
- `docs:` documentation
- `refactor:` code restructuring
- `test:` adding tests
- `chore:` maintenance
5. Create commit with the message, adding Co-Authored-By trailer
## Commit Message Format
```
<type>(<scope>): <short description>
<body - what and why, not how>
Co-Authored-By: Claude <noreply@anthropic.com>
```
## Best Practices
- Warn about large commits (>500 lines changed)
- Suggest splitting large changes into smaller commits
- Never skip pre-commit hooks unless explicitly requested
## Changelog
After committing, update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format:
1. Add entry under `[Unreleased]` section (create if not exists)
2. Categorize changes:
- `Added` - new features
- `Changed` - changes to existing functionality
- `Deprecated` - features marked for removal
- `Removed` - removed features
- `Fixed` - bug fixes
- `Security` - security-related changes
3. Write entries in imperative mood: "Add feature" not "Added feature"
4. Reference issue numbers where applicable
+104
View File
@@ -0,0 +1,104 @@
---
name: penpot-login
description: Connect to Penpot MCP - starts the MCP server, logs into penpot.schweitz.net, and installs/connects the plugin
allowed-tools: Read, Bash, mcp__chrome-devtools__new_page, mcp__chrome-devtools__navigate_page, mcp__chrome-devtools__take_snapshot, mcp__chrome-devtools__take_screenshot, mcp__chrome-devtools__click, mcp__chrome-devtools__fill, mcp__chrome-devtools__press_key, mcp__chrome-devtools__list_pages
---
# Penpot MCP Connection
This skill connects Claude to Penpot by:
1. Starting the Penpot MCP server (if not running)
2. Logging into penpot.schweitz.net via Authentik
3. Installing and connecting the Penpot MCP plugin
## Step 1: Start MCP Server
Check if the MCP server is running, start it if not:
```bash
# Check if server is running
if ! curl -s http://localhost:9880/mcp > /dev/null 2>&1; then
# Start the server in background
cd ~/Projects/penpot-mcp && ./start-server.sh &
sleep 3 # Wait for server to start
fi
```
Server endpoints when running:
- MCP Server: http://localhost:9880/mcp
- Plugin Server: http://localhost:9879/
- WebSocket: ws://localhost:4402
## Step 2: Login Credentials
Read credentials from the project root file `claude-authentik-credentials.md`:
!`cat claude-authentik-credentials.md`
## Step 3: Login Flow
1. **Navigate to Penpot**
- Use `mcp__chrome-devtools__new_page` to go to `https://penpot.schweitz.net`
2. **Check if already logged in**
- Take a snapshot
- If you see "Projects" heading, you're logged in - skip to Step 4
- If you see login page, continue with authentication
3. **Authenticate via Authentik** (if not logged in)
- Click the OpenID button to redirect to Authentik
- On auth.schweitz.net, fill username textbox
- Click "Log in" button
- Fill password textbox
- Click "Continue" button
- If redirected to Authentik user page instead of Penpot, navigate back to `https://penpot.schweitz.net`
## Step 4: Open Design File
1. **Navigate to the Clide project**
- Double-click on the TUI file to open the workspace
## Step 5: Install/Connect Plugin
1. **Open Plugins menu**
- Click the Plugins button (puzzle icon, keyboard shortcut: Cmd+Alt+P)
2. **Check if plugin is installed**
- If "Penpot MCP Plugin" appears under "INSTALLED PLUGINS", click OPEN
- Otherwise, install it first:
3. **Install plugin** (if not installed)
- Fill the plugin URL textbox with: `http://localhost:9879/manifest.json`
- Click INSTALL
- Click ALLOW on the permissions dialog
4. **Connect to MCP server**
- In the plugin UI, click "CONNECT TO MCP SERVER"
- Verify it shows "Connected to MCP server"
## Step 6: Configure Claude Code MCP
Add the Penpot MCP server to Claude Code (if not already configured):
```bash
claude mcp add penpot -t http http://localhost:9880/mcp
```
**Important**: Use HTTP transport (`-t http`) with the `/mcp` endpoint. Do NOT use SSE transport - it causes "Server not initialized" errors.
## Step 7: Verify Connection
After connecting, restart the MCP connection in Claude Code:
- Use `/mcp` command to reconnect the penpot server
- The Penpot MCP tools (execute_code, export_shape, etc.) will then be available
Test with:
```javascript
mcp__penpot__execute_code(code="return penpot.currentPage.name;")
```
## Important Notes
- Keep the Penpot plugin UI window open while using MCP tools
- The MCP server must be running for the plugin to connect
- If connection fails, check that the server is running on port 9880
- Use HTTP transport (`-t http`), NOT SSE transport