Automating Login with Playwright MCP and Claude Code
When you automate browser work by combining Claude Code with Playwright MCP, the recurring problem is how to run “the same login steps as always” against a test environment with as few tool round trips as possible, in a form that later operations can build on.
This article walks through how I arrived at a design that runs a real Playwright script end to end on the same page via browser_run_code_unsafe, along with the benefits, the risks, and the alternatives I rejected. It is written as a general automation pattern rather than something tied to any particular service.
What was I trying to automate?
Making a repeated login sequence routine
The target was a standard login to a staging environment under test. The content is nearly identical every time, and it repeats this sequence:
- Overriding the user agent and headers to get past a WAF or similar front layer
- Checking whether access has been refused with a 403
- Logging out of any existing session
- Entering the email and password into the login form and submitting
- Confirming the transition to the post-login page
Doing this step by step from Claude Code through Playwright MCP produces a long run of tool calls. Every extra round trip costs both time and session budget, so I looked for a design that minimises round trips while keeping the result easy to build on.
Credentials never touch the disk
A working constraint: credentials — email, password, bypass tokens — are never saved to a file in plain text. Everything is fetched from a secure store each time and handled only in memory.
What design did I settle on?
The runtime flow
What I adopted was pouring a templated Playwright script onto the same page in a single tool call. At runtime it is four steps:
- Fetch the credentials from the secure store (email, password, bypass token)
- Load a static template JS file — real Playwright code that takes
pageand runs — and safely substitute the placeholders (__EMAIL__and so on) with the fetched values, escaping viaJSON.stringify. That produces a finished JS code string - Call
browser_run_code_unsafeexactly once, passing the finished code in thecodeparameter. Inside the same browser and page it runs the whole sequence: UA override, 403 check, logout, form entry, submit, transition check - Read the return value (success or failure, destination URL) and move on to the steps specific to whatever you are actually doing, using ordinary
browser_snapshotandclickcalls
The point is that the tool call count comes down to essentially two: one to fetch credentials, one for run_code_unsafe.
The template is working code, not a written procedure
The heart of this design is that the template is not prose instructions but an actual Playwright script that takes page and runs. Substituting real values for the placeholders is all it takes to make it executable.
What are the benefits?
Far fewer round trips, with no handover problem
The main advantage is getting both the reduction in round trips and the ease of carrying the browser forward.
- A real Playwright script: the template is code that runs, not a procedure to be interpreted. There is no room for interpretation to drift
- Round trips collapse: the sequential-tool-call approach took six or seven; this takes two, cutting both elapsed time and session consumption
- No browser handover problem:
browser_run_code_unsaferuns on the same page that later operations use. The concern with the separate Node script option — that the login happens in a different browser and cannot be carried forward — does not arise - Nothing extra to install: the script runs inside the Playwright MCP server’s own process, so the machine running it does not need its own Node.js or Playwright. MCP is already a prerequisite, so no new environment dependency appears
- Secrets never reach the disk: credentials are passed in memory through the
codeparameter and never written to a file
What are the drawbacks and risks?
This is a high-privilege tool
There are risks that should not be brushed aside, and the most important is that browser_run_code_unsafe carries privileges equivalent to arbitrary code execution. Understand that before you put it into regular use.
- Effectively RCE-level privilege:
browser_run_code_unsafeis officially described as equivalent to remote code execution. Depending on your environment’s permission mode, an approval prompt on first use is likely, and it draws more scrutiny than ordinary clicks and typing - Unverified selectors: the selectors for the email field, password field and submit button may have been inferred from a written procedure rather than confirmed against the real DOM. Expect the first run to fail and to need adjustment against the actual page
- The substitution step never goes away: replacing placeholders with real values is code generation, so a mistake there becomes a runtime error. Since the values change every time, some assignment step will always remain
- Fetching credentials still costs something: as long as you refuse to store them in plain text, one fetch per run remains — though it is a single fetch, not repeated probing
- Consoles requiring human approval are out of scope: an admin console behind SAML plus multi-factor authentication that requires a person to approve cannot be automated by this script. Reaching that point and asking a human still applies
How does it compare with the alternatives?
Several options were compared before landing on this one. Lining them up on round trips, handover, and machine dependency makes the differences clear.
| Option | What it is | Round trips | Handover | Machine dependency |
|---|---|---|---|---|
| Original idea (written procedure) | A Markdown procedure, executed as sequential tool calls | Many (6–7) | Fine | None |
| Option A (single evaluate) | Consolidated into one browser_evaluate call |
Few (2–3) | Fine | None |
| Option B (separate Node script) | A fully independent script run through Bash | Fewest (1) | Different browser, cannot be carried forward | Needs Node and Playwright installed |
| Adopted | run_code_unsafe executing on the same page |
Few (2) | Fine | None (runs inside the MCP process) |
The adopted option gets essentially Option B’s speed while keeping Option A’s handover behaviour and independence from the local machine.
Summary: balancing speed against handover
If you are automating a routine login with Claude Code and Playwright MCP, pouring a templated, real Playwright script onto the same page through browser_run_code_unsafe was the practical answer for getting few round trips and easy browser handover at the same time.
That said, the tool carries privileges equivalent to arbitrary code execution, so approval flows and real-world selector verification cannot be skipped. Keep secrets off the disk and pass them in memory each time, and stay careful with a high-privilege tool — those two points are what make this safe to run regularly.
Note: this describes the thinking at design time. Tool permission models and APIs may change. Handle credentials according to your own environment’s security policy.