Configuring Claude Code Playwright MCP on NixOS
Getting Claude Code’s Playwright MCP (Model Context Protocol) to work on NixOS presents several unique challenges. This guide documents a complete declarative solution using home-manager.
The Problem
NixOS’s unique architecture creates several issues for Playwright MCP:
- Browser Installation Issues: Standard
npx playwright installdoesn’t work with NixOS’s non-FHS layout - Read-only Nix Store: Playwright tries to create user data directories inside
/nix/store, which is read-only - Browser Detection: Even with
PLAYWRIGHT_BROWSERS_PATHset, the MCP server doesn’t recognize browsers as “installed” - Non-persistent Configuration: Manual changes to
~/.claude.jsonget overwritten on system updates
Typical errors you’ll encounter:
Error: Browser specified in your config is not installed. Either install it (likely) or change the config.
Or:
ENOENT: no such file or directory, mkdir '/nix/store/.../playwright-browsers/mcp-chrome-...'
The Solution
The solution involves:
- Using
playwright-web-flaketo provide browsers in the Nix store - Creating a home-manager module that declaratively manages Claude Code’s MCP configuration
- Using an activation script to merge configuration into
~/.claude.jsonon every rebuild - Specifying the exact executable path to bypass browser detection issues
- Setting a writable profile directory for browser user data
Implementation
Step 1: Add playwright-web-flake to your flake inputs
In your flake.nix:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
# Add Playwright browser flake
playwright-web-flake.url = "github:pietdevries94/playwright-web-flake";
};
outputs = { nixpkgs, home-manager, playwright-web-flake, ... }@inputs: {
# Your existing configuration...
};
}
Step 2: Install required packages
In your home-manager/home.nix:
{ inputs, pkgs, ... }:
{
home.packages = with pkgs; [
claude-code
inputs.playwright-web-flake.packages.${pkgs.system}.playwright-driver
];
home.sessionVariables = {
PLAYWRIGHT_BROWSERS_PATH = "${inputs.playwright-web-flake.packages.${pkgs.system}.playwright-driver.browsers.${pkgs.system}}";
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = "true";
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
};
}
Step 3: Create the Claude Code configuration module
Create home-manager/programs/claude-code.nix:
{ inputs, lib, config, pkgs, ... }:
let
playwrightDriver = inputs.playwright-web-flake.packages.${pkgs.system}.playwright-driver;
playwrightBrowsersPath = builtins.unsafeDiscardStringContext "${playwrightDriver.browsers}";
playwrightMcpConfig = {
playwright = {
type = "stdio";
command = "npx";
args = [
"@playwright/mcp@latest"
"--browser"
"chromium"
"--executable-path"
"$PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"
];
env = {
PLAYWRIGHT_BROWSERS_PATH = "$PLAYWRIGHT_BROWSERS_PATH";
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = "true";
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
PWMCP_PROFILES_DIR_FOR_TEST = "$HOME/.local/share/playwright-mcp/profiles";
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = "$PLAYWRIGHT_BROWSERS_PATH/chromium-1181/chrome-linux/chrome";
};
};
};
mcpConfigJson = builtins.toJSON playwrightMcpConfig;
in
{
home.activation.configureClaude = lib.hm.dag.entryAfter ["writeBoundary"] ''
CLAUDE_CONFIG="$HOME/.claude.json"
if [ ! -f "$CLAUDE_CONFIG" ]; then
echo '{}' > "$CLAUDE_CONFIG"
fi
if [ -f "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" ]; then
source "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh"
fi
MCP_CONFIG=$(echo '${mcpConfigJson}' | ${pkgs.gnused}/bin/sed \
-e "s|\$PLAYWRIGHT_BROWSERS_PATH|$PLAYWRIGHT_BROWSERS_PATH|g" \
-e "s|\$HOME|$HOME|g" \
-e "s|\$PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH|$PLAYWRIGHT_BROWSERS_PATH/chromium-1181/chrome-linux/chrome|g")
${pkgs.jq}/bin/jq --argjson mcpServers "$MCP_CONFIG" \
'.mcpServers = $mcpServers' \
"$CLAUDE_CONFIG" > "$CLAUDE_CONFIG.tmp" && \
mv "$CLAUDE_CONFIG.tmp" "$CLAUDE_CONFIG"
$VERBOSE_ECHO "Claude Code Playwright MCP configuration updated"
'';
}
Step 4: Import the module
In your home-manager/home.nix, add to the imports list:
{
imports = [
./programs/claude-code.nix
];
}
Step 5: Apply the configuration
home-manager switch --flake .#your-user@your-hostname
Restart Claude Code completely to pick up the new MCP configuration.
How It Works
- Declarative Configuration: MCP server settings are defined in Nix and automatically applied on every
home-manager switch - Dynamic Path Resolution: The activation script sources environment variables and expands them at runtime
- Non-destructive Updates: Uses
jqto merge only themcpServerssection, preserving all other Claude Code settings - Executable Path Override:
--executable-pathbypasses Playwright’s browser detection - Writable Profile Directory:
PWMCP_PROFILES_DIR_FOR_TESTredirects browser profiles to a writable location
Verification
After configuration:
# Check MCP server status
claude mcp list
# Check configuration
cat ~/.claude.json | jq '.mcpServers.playwright'
# Check environment variables
echo $PLAYWRIGHT_BROWSERS_PATH
# Verify browser exists
ls -la $PLAYWRIGHT_BROWSERS_PATH/chromium-1181/chrome-linux/chrome
Test in Claude Code:
Navigate to example.com and tell me what you see
Key Insights
Why --executable-path is Required
Setting PLAYWRIGHT_BROWSERS_PATH alone isn’t enough because Playwright MCP uses its own browser detection mechanism. By providing --executable-path, we bypass this check entirely and tell Playwright exactly where the browser binary is.
Why the Activation Script Sources Environment Variables
The activation script runs during home-manager switch, before the new environment variables are fully exported to the shell. By explicitly sourcing hm-session-vars.sh, we ensure PLAYWRIGHT_BROWSERS_PATH is available for path expansion.
The NixOS ENOENT/EACCES Issue
Playwright MCP tries to create user data directories inside the browsers directory by default. On NixOS, this fails because /nix/store is read-only. The workaround is setting PWMCP_PROFILES_DIR_FOR_TEST to point to a writable location.
Benefits
- ✅ Survives System Updates: Configuration is declarative and automatically reapplied
- ✅ No Manual Browser Installation: Browsers are provided by Nix
- ✅ Reproducible: Same configuration works across different NixOS machines
- ✅ Version Controlled: MCP configuration is part of your nix-config repository
- ✅ Non-destructive: Preserves Claude Code’s self-managed settings
Troubleshooting
“Browser specified in your config is not installed”
- Verify
PLAYWRIGHT_BROWSERS_PATHis set:echo $PLAYWRIGHT_BROWSERS_PATH - Check the browser exists:
ls -la $PLAYWRIGHT_BROWSERS_PATH/chromium-1181/chrome-linux/chrome - Ensure you’ve restarted Claude Code after applying configuration
- Verify the
--executable-pathargument is in~/.claude.json
“ENOENT: no such file or directory, mkdir ‘/nix/store/…’”
- Ensure
PWMCP_PROFILES_DIR_FOR_TESTis set in the environment variables - Check that
~/.local/share/playwright-mcp/profilesdirectory exists and is writable
Browser version mismatch
The hardcoded chromium-1181 in the executable path may need updating when Playwright versions change:
ls -la $PLAYWRIGHT_BROWSERS_PATH/ | grep chromium
Update the version number in claude-code.nix accordingly.
Resources
Tested with: Claude Code, Playwright MCP 0.0.42, NixOS unstable