Skip to main content

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:

  1. Browser Installation Issues: Standard npx playwright install doesn’t work with NixOS’s non-FHS layout
  2. Read-only Nix Store: Playwright tries to create user data directories inside /nix/store, which is read-only
  3. Browser Detection: Even with PLAYWRIGHT_BROWSERS_PATH set, the MCP server doesn’t recognize browsers as “installed”
  4. Non-persistent Configuration: Manual changes to ~/.claude.json get 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:

  1. Using playwright-web-flake to provide browsers in the Nix store
  2. Creating a home-manager module that declaratively manages Claude Code’s MCP configuration
  3. Using an activation script to merge configuration into ~/.claude.json on every rebuild
  4. Specifying the exact executable path to bypass browser detection issues
  5. 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

  1. Declarative Configuration: MCP server settings are defined in Nix and automatically applied on every home-manager switch
  2. Dynamic Path Resolution: The activation script sources environment variables and expands them at runtime
  3. Non-destructive Updates: Uses jq to merge only the mcpServers section, preserving all other Claude Code settings
  4. Executable Path Override: --executable-path bypasses Playwright’s browser detection
  5. Writable Profile Directory: PWMCP_PROFILES_DIR_FOR_TEST redirects 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_PATH is 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-path argument is in ~/.claude.json

“ENOENT: no such file or directory, mkdir ‘/nix/store/…’”

  • Ensure PWMCP_PROFILES_DIR_FOR_TEST is set in the environment variables
  • Check that ~/.local/share/playwright-mcp/profiles directory 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