When developing userscripts iteratively with a user, use localStorage as a persistent workspace to store code chunks.
This allows you to:
- Persist code across page reloads - No need to regenerate code which is currently working
- Iterate incrementally - Fix/update individual chunks without rewriting everything (saves tokens and time)
- Test quickly - Reload page and re-execute all chunks in page context nearly instantaneously with a single command
- Easy export - User can retrieve all code instantly using Agent-provided console command(s)
This document never uses the ASCII Backslash character (0x5C, String.fromCodePoint(92)). Instead, it uses ╲ (U+2572) as a stand-in.
╲ is a documentation-only convention; it should not appear in real code.
Any code block containing ╲ is not copy/paste runnable: before running it, replace each ╲ with the same number of ASCII Backslashes.
When using the javascript_tool to execute code, ASCII Backslashes get "eaten" by escape processing layers. If you write ╲s (for a regex whitespace character), it arrives as just s in the executed code.
Use 4 ASCII Backslashes in your tool parameter to get 1 ASCII Backslash in the final executed JavaScript:
| You write in tool param | What executes in browser |
|---|---|
╲╲╲╲s |
╲s |
╲╲╲╲n |
╲n |
╲╲╲╲t |
╲t |
/╲╲╲╲s+/ |
/╲s+/ |
Wrong (ASCII Backslash gets eaten):
const components = str.split(/╲s+/); // Becomes /s+/ - breaks!Correct:
const components = str.split(/╲╲╲╲s+/); // Becomes /╲s+/ - works!Two layers of escape processing:
- Tool parameter transmission (4 ASCII Backslashes -> 2 ASCII Backslashes)
- Template literal / string processing (2 ASCII Backslashes -> 1 ASCII Backslash)
To verify what's actually arriving, check the character codes:
(function() {
const code = `const x = /╲╲╲╲s+/;`;
return Array.from(code).map(c => c.charCodeAt(0));
})();Look for 92 (ASCII Backslash) in the output. If it's missing, you need more ASCII Backslashes.
If escaping is too confusing, use alternatives that don't require ASCII Backslashes:
- Instead of
/╲s+/for whitespace split, usesplit(' ').filter(c => c.length > 0) - Instead of
╲nfor newline, useString.fromCodePoint(10) - Instead of
╲tfor tab, useString.fromCodePoint(9)
Store individual pieces of the userscript under keys prefixed with claude_dev_chunk:
For example, a recent coding session used these chunks:
claude_dev_chunk:! - Chunks are sorted, so the "!" chunk will always inject first
claude_dev_chunk:styles - CSS styles
claude_dev_chunk:hideElements - DOM manipulation to hide elements
claude_dev_chunk:keyboard - Keyboard shortcuts
claude_dev_chunk:lightbox - Modal/dialog functionality
claude_dev_chunk:menuBar - Menu bar modifications
claude_dev_chunk:output - Output area setup
claude_dev_chunk:codemirror - CodeMirror-specific configuration
claude_dev_chunk:~ - Chunks are sorted, so the "~" chunk will always inject last
The number of chunks and the names of chunks should vary based on the complexity and nature of the task. The localStorage strategy is primarily about enabling the agent to iterate quickly and spend fewer tokens. Don't feel you have to stick to a particular naming convention or organizational strategy. For example, you may want to split out sections of code that are under heavy development into separate chunks for easier iteration. For some tasks, you might want to have a single chunk per function. Use whatever strategy makes the most sense for your current task. You can also "refactor" the chunks at any time by copying values between localStorage items, concatenating, renaming, splitting, deleting, etc. You can also make "surgical edits" to individual chunks using regex search/replace or other string manipulation techniques -- conceptually a bit like applying a patch.
The contents of each chunk is just a string containing JavaScript code. The code could be passed to eval() or used as the body of a document.createElement('script').
Store a userscript generator under the localStorage key claude_dev_assemble_userscript.
This function concatenates all the chunks and assembles them into a userscript (.user.js):
(() => {
let header = `
// ==UserScript==
// @name {{userscriptName}}
// @namespace https://greasyfork.org/en/users/1337417-mevanlc
// @version 0.1
// @description {{userscriptDescription}}
// @author mevanlc
// @match {{urlMatchPattern}}
// ((often https://*.domain.com/* will do))
// ((rarely necessary, but when needed, multiple @match lines can be used))
// @grant none
// ((use none unless there's a specific reason to use a different value))
// @run-at document-end
// ((use document-end unless there's a specific reason to use a different value))
// @license MIT
// ==/UserScript==
`;
let claudeDevChunkNames = Object.keys(localStorage).filter(k => k.startsWith('claude_dev_chunk:')).sort();
let body = claudeDevChunkNames.map(k => localStorage.getItem(k)).join('╲n╲n');
return header + '╲n' + body;
})();// dogfooding: While the agent is developing, the agent will need to run their code.
// If the agent runs their code via claude_dev_assemble_userscript, the agent can be sure
// they are testing the exact same code that will be in the final userscript.
eval(localStorage.getItem('claude_dev_assemble_userscript'));// when the userscript is complete, user can retrieve the userscript with:
copy(eval(localStorage.getItem('claude_dev_assemble_userscript')));- Freelance analyze the page structure with
read_page,javascript_tool, and screenshots - Begin creating chunks of reusable code with observable effects that allow for verification (think Unix philosophy)
- Periodically (or as often as you like), reload the page to reset the DOM state and verify that the code, as it is, runs correctly from a fresh state
- In some cases on some pages, reloading may be disruptive or slow, so use judgment.
Prefer "reloading" by explicitly navigating to the original URL. This is because calling .reload() may trigger SPA-style modifications to the URL that the app might make during dynamic interaction with the page.
Writing Userscripts with Claude Chrome - some potentially useful patterns (not all are necessary in all cases):
Debugging Techniques:
- Use sentinel attributes (
x-userjs-*) on DOM elements to trace tricky execution or avoid double-processing - Add
DEBUGflag with conditionallogdebug()function
Gotchas:
- Form submission: Use
addEventListener('submit', ...)not button click handlers - Target specific forms: Use class selectors like
.home-searchto avoid conflicts
const chunk_styles = `(function() {
const style = document.createElement('style');
style.id = 'my-userscript-styles';
style.textContent = ╲`
.my-class { display: none !important; }
#myElement { position: fixed !important; }
╲`;
document.head.appendChild(style);
return 'Styles injected';
})();`;const chunk_hideElements = `(function() {
// Hide elements
document.querySelectorAll('.unwanted').forEach(el => el.style.display = 'none');
// Modify text
const label = document.querySelector('#myLabel');
if (label) label.textContent = 'New Label';
return 'Elements modified';
})();`;const chunk_keyboard = `(function() {
document.addEventListener('keydown', function(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
myFunction();
}
});
return 'Keyboard shortcuts added';
})();`;const chunk_waitFor = `(function() {
setTimeout(() => {
const el = document.querySelector('.dynamic-element');
if (el) {
// Do something
}
}, 500);
return 'Deferred setup scheduled';
})();`;Claude Chrome sometimes refuses to return strings to Claude if that string
contains = in certain contexts (often with a warning about 🍪 content). If
blocking happens, instead of evaluating/returning <expr> directly, use this
escape, then mentally substitute = back to = when reading results:
<expr>.replaceAll('=', '=')Treat = as a transport-only escape; downstream actions should use the
real = character, not =.
read_page and get_page_text often don't show .value for form elements
(for example TEXTAREA, INPUT). Use javascript_tool:
document.querySelector('#myTextarea').value- Don't assume you're done, verify!
- Reload
- Test the intended functionality
- Take screenshots at key stages, especially the final stage
- Look for discrepancies
- Do not "hand off" the code by repeating it into chat -- instead, guide the
user to retrieve it from localStorage via
copy(), etc.
If you've iterated on the same problem ~20 times, it might be time to use the user as your rubber duck. Describe the challenge and ask for ideas. They might have something that helps, or you might just have to get back to the grindstone on your own lights, but either way, stepping back for a moment can help.
If the user asks for help with publishing the userscript, open a tab (or reuse an existing 'documentation' tab) and read this guide document:
https://gist.github.com/mike-clark-8192/0c2e3e7fa248c8c6688094b5d5ac9597
It contains lots of very useful information for that task, so don't skip it if they ask about publishing!
After reading this document, do not summarize or repeat the content of this document for the user -- the user is the author of this document and is well-aware of its contents. The acknowledgment you should give is something like: "I have read and understood the Claude Chrome Userscript Guidance document, what's next?"