Claude Code Workflows Part 1: Visual Debugging on a Headless Server
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
- Visual Debugging on a Headless Server (you are here)
- Skills & Automation (coming soon)