Skip to content
Back to blog

Claude Code Workflows Part 1: Visual Debugging on a Headless Server

8 min read
View as Markdown

When you develop on a remote headless server, there's no browser to open. You can't just Cmd+Tab to Chrome and check how your component looks. I needed a way to visually debug UI changes directly from Claude Code — take a screenshot, see it instantly, and iterate.

Here's the system I built: Claude Code takes screenshots with Playwright MCP, uploads them to a self-hosted image service, and gives me shareable links I can open on any device. Screenshots auto-delete after 24 hours because they're throwaway by nature.

The Architecture

flowchart TD
  A["Claude Code"] -->|"1. Navigate to page"| B["Dev Server\nlocalhost:3000\n(Headless Chromium)"]
  A -->|"2. Take screenshot"| C["screenshot.png"]
  C -->|"3. imgup upload"| D["img.myapp.com\nNginx + SSL\n24h auto-cleanup"]
  D -->|"4. Returns shareable URL"| A

Step 1: Set Up the Image Service

First, we need somewhere to host screenshots. I run a simple Nginx file server on a subdomain that serves static images over HTTPS.

Create the Directory

sudo mkdir -p /var/www/img.myapp.com
sudo chown $USER:$USER /var/www/img.myapp.com

Configure Nginx

sudo tee /etc/nginx/sites-available/img.myapp.com << 'EOF'
server {
    server_name img.myapp.com;

    root /var/www/img.myapp.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
        add_header Cache-Control "public, max-age=3600";
    }

    # Disable directory listing
    autoindex off;

    # Only serve image files
    location ~* \.(png|jpg|jpeg|gif|webp|svg|ico)$ {
        expires 1d;
        add_header Cache-Control "public, immutable";
    }

    listen 80;
}
EOF

sudo ln -s /etc/nginx/sites-available/img.myapp.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Key security details:

  • autoindex off — prevents anyone from browsing the directory listing
  • Image-only location block — only serves known image file types
  • Cache headers — images are cached for 1 day since they're temporary anyway

Add SSL

sudo certbot --nginx -d img.myapp.com

Create the imgup Command

Add this function to your ~/.zshrc:

imgup() {
  if [ -z "$1" ]; then
    echo "Usage: imgup <file>"
    return 1
  fi
  local file="$1"
  local name="$(date +%s)_$(basename "$file")"
  cp "$file" "/var/www/img.myapp.com/$name"
  echo "https://img.myapp.com/$name"
}

Now you can upload any image:

imgup screenshot.png
# https://img.myapp.com/1741520400_screenshot.png

The timestamp prefix prevents filename collisions. The URL is immediately shareable — open it on your phone, send it to a colleague, or paste it in a PR.

Auto-Delete After 24 Hours

Screenshots are throwaway. Set up a cron job to clean them up automatically:

crontab -e

Add this line:

0 * * * * find /var/www/img.myapp.com -type f -mmin +1440 -delete

This runs every hour and deletes any file older than 24 hours (1440 minutes). Your image directory stays clean without manual intervention.

Step 2: Enable Playwright MCP in Claude Code

Claude Code has a Playwright MCP plugin that controls a headless Chromium browser. It can navigate to pages, click elements, fill forms, and — crucially — take screenshots.

Install Chromium

Playwright needs a browser to control. Install Chromium and its dependencies:

npx playwright install chromium

On a headless Ubuntu server, you might also need system dependencies:

npx playwright install-deps chromium

Enable the Plugin

In Claude Code, the Playwright plugin is available as a built-in MCP server. Enable it in your ~/.claude/settings.json:

{
  "enabledPlugins": {
    "playwright@claude-plugins-official": true
  }
}

Restart Claude Code after enabling the plugin.

Auto-Approve Playwright Tools

To avoid being asked for permission every time Claude Code takes a screenshot, add the Playwright MCP tools to your allowed permissions in ~/.claude/settings.local.json:

{
  "permissions": {
    "allow": [
      "mcp__plugin_playwright_playwright__browser_navigate",
      "mcp__plugin_playwright_playwright__browser_snapshot",
      "mcp__plugin_playwright_playwright__browser_take_screenshot",
      "mcp__plugin_playwright_playwright__browser_resize",
      "mcp__plugin_playwright_playwright__browser_wait_for",
      "mcp__plugin_playwright_playwright__browser_click",
      "mcp__plugin_playwright_playwright__browser_type"
    ]
  }
}

Step 3: Create the Screenshot Skill

The manual workflow is: navigate to a page, resize the viewport, take a screenshot, run imgup, report the URL. That's 5 steps every time. A Claude Code skill automates this into a single command.

What Are Skills?

Skills are markdown files that teach Claude Code reusable workflows. When you say a trigger phrase, Claude Code loads the skill and follows its instructions. They live in ~/.claude/skills/.

Create the Skill

mkdir -p ~/.claude/skills/imgup-screenshot

Create ~/.claude/skills/imgup-screenshot/SKILL.md:

---
name: imgup-screenshot
description: Take screenshots of webpages and upload them for remote preview. Trigger phrases include "take screenshot", "preview the page", "show me how it looks", or "imgup".
---

# imgup-screenshot

Take screenshots of webpages and upload them for remote preview on headless servers.

## Workflow

### 1. Navigate to the Target Page

Use Playwright MCP to navigate to the URL:

mcp__plugin_playwright_playwright__browser_navigate(url: "http://localhost:3000")

Wait for the page to load if needed:

mcp__plugin_playwright_playwright__browser_wait_for(time: 2)

### 2. Take Desktop Screenshot (1280x720)

Resize the browser to desktop viewport:

mcp__plugin_playwright_playwright__browser_resize(width: 1280, height: 720)

Take the screenshot:

mcp__plugin_playwright_playwright__browser_take_screenshot(type: "png", filename: "desktop.png")

### 3. Take Mobile Screenshot (375x667)

Resize the browser to mobile viewport:

mcp__plugin_playwright_playwright__browser_resize(width: 375, height: 667)

Take the screenshot:

mcp__plugin_playwright_playwright__browser_take_screenshot(type: "png", filename: "mobile.png")

### 4. Upload Screenshots

Upload each screenshot using the imgup command:

imgup desktop.png
imgup mobile.png

Each command returns a URL like https://img.myapp.com/1234567890_desktop.png

### 5. Present Results

Report both URLs to the user:

Desktop (1280x720): https://img.myapp.com/...desktop.png
Mobile (375x667): https://img.myapp.com/...mobile.png

## Viewport Reference

| Device  | Width | Height |
|---------|-------|--------|
| Mobile  | 375   | 667    |
| Desktop | 1280  | 720    |

## Notes

- Screenshots are saved to the current working directory
- The imgup command copies files to the local server (auto-deleted after 24 hours)
- Full-page screenshots can be taken with fullPage: true parameter

Tell Claude Code About imgup

Add a note to your ~/.claude/CLAUDE.md so Claude Code knows about the tool even without the skill:

### imgup - Image Upload CLI
Upload images to local server and get shareable links:

imgup /path/to/image.png
# Returns: https://img.myapp.com/1234567890_image.png

Files are served from /var/www/img.myapp.com/ and auto-deleted after 24 hours.
Use with Playwright screenshots to share with user for review.

Using It in Practice

Once everything is set up, the workflow is natural. Here's how a typical visual debugging session goes:

Quick Preview

You: take a screenshot of the homepage

Claude Code:
  → Navigates to http://localhost:3000
  → Takes desktop screenshot (1280x720)
  → Takes mobile screenshot (375x667)
  → Uploads both with imgup
  → Returns:
    Desktop: https://img.myapp.com/1741520400_desktop.png
    Mobile: https://img.myapp.com/1741520401_mobile.png

Open the links on your phone, tablet, or any browser. No port forwarding needed for a quick visual check.

Iterative Design

This is where it gets powerful. You can have Claude Code make changes and immediately verify them:

You: the navbar text is too small on mobile, make it bigger

Claude Code:
  → Edits the component
  → Takes a new mobile screenshot
  → Uploads it
  → "Here's the updated mobile view: https://img.myapp.com/..."
  → You check the link
  → "looks good but the padding is off on the right"
  → Fixes padding, takes another screenshot
  → Repeat until it looks right

You never leave the terminal. Claude Code sees the page through Playwright, makes changes, and shows you the result — all in one conversation.

Comparing Before and After

When reviewing a PR or making visual changes, you can ask for comparison screenshots:

You: take a screenshot of /blog before and after your changes

Claude Code:
  → Stashes changes, screenshots the original
  → Restores changes, screenshots the updated version
  → Uploads both
  → "Before: https://img.myapp.com/...before.png"
  → "After: https://img.myapp.com/...after.png"

Checking Specific Elements

Playwright MCP can do more than full-page screenshots. You can interact with the page:

You: open the mobile menu and screenshot it

Claude Code:
  → Resizes to mobile viewport
  → Clicks the hamburger menu button
  → Waits for animation
  → Takes screenshot
  → Uploads it

Why Not Just Use Port Forwarding?

Port forwarding (via Termius or SSH) works great for live development — you see changes in real-time in your browser. But screenshots fill a different role:

  • Shareable — send a link to anyone, no VPN or SSH access needed
  • Persistent — the screenshot exists for 24 hours, even after you stop the dev server
  • CI/CD friendly — you can add screenshot steps to your GitHub Actions workflow
  • Reviewable — paste screenshot links directly in PR descriptions
  • No setup on the viewing device — any browser on any device can open the link

I use port forwarding for active development and screenshots for reviewing, sharing, and documenting changes.

What's Next

In the next post, we'll dive deeper into Claude Code skills — how to build them, the ones I use daily, and how they turn repetitive workflows into single commands.

Series Navigation

  1. Visual Debugging on a Headless Server (you are here)
  2. Skills & Automation (coming soon)