# Claude Code Workflows Part 1: Visual Debugging on a Headless Server Date: 2026-03-09 · 8 min read Tags: claude-code, playwright, debugging, remote-development URL: https://ahmedelywa.com/blog/claude-code-workflows-part-1-visual-debugging --- 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 ```mermaid 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 ```bash sudo mkdir -p /var/www/img.myapp.com sudo chown $USER:$USER /var/www/img.myapp.com ``` ### Configure Nginx ```bash 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 ```bash sudo certbot --nginx -d img.myapp.com ``` ### Create the imgup Command Add this function to your `~/.zshrc`: ```bash imgup() { if [ -z "$1" ]; then echo "Usage: imgup " 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: ```bash 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: ```bash 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: ```bash npx playwright install chromium ``` On a headless Ubuntu server, you might also need system dependencies: ```bash 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`: ```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`: ```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 ```bash mkdir -p ~/.claude/skills/imgup-screenshot ``` Create `~/.claude/skills/imgup-screenshot/SKILL.md`: ```markdown --- 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: ```markdown ### 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](/blog/claude-code-workflows-part-2-skills-and-automation), 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](/blog/claude-code-workflows-part-2-skills-and-automation) (coming soon)