Letting Claude Code operate a production server means it can take over everything from code fixes to log investigation, but a single command can also take a site down.
I run six websites, including this one (ryusei.io), on a single mini PC at home, and I manage all of them with Claude Code, from code fixes and article publishing to server administration. I wanted a separate development environment, but machine prices had gone up with rising memory and storage costs, so I put it off and use Claude Code directly on the production server. If the budget allows, I plan to add a development machine as well. Because editing the source changes the production files immediately, I have added instruction files and confirmation steps each time an incident happened.
This article explains the whole setup I have built up that way.
What You'll Learn from This Article
- How to split CLAUDE.md for a production server
- How to make Claude Code ask for confirmation before risky operations
- How to restrict permissions for unattended Claude Code runs
- How to carry knowledge across sessions
- How to put repeated procedures into Claude Code skills
This article assumes you already know the basics of Claude Code, such as installing it and what CLAUDE.md is for. It does not cover how to build a home server or expose it to the internet.
How I Use Claude Code on a Production Server
Each site runs in its own Docker container. Visitors reach the home server through a VPS that serves as the IPv4 entry point.
I work with Claude Code by keeping Remote Control (claude rc) running on the server and opening sessions from the Claude Desktop app on my PC or the Claude app on my phone. I can open the same session from either device and continue where I left off. I connect to the server over SSH only when I want to run commands myself or need to work in a terminal.
What is claude rc (Remote Control)?
claude rc (claude remote-control) is a command that lets you control Claude Code sessions running on this machine from claude.ai/code in a browser or from the Claude apps. When you run it in the working folder, it waits for connections, and when you open a session from another device, the session starts on this machine. All file reads and writes and all command execution happen on this machine; the other devices only display the screen and take input.
This machine connects out to Anthropic's servers and waits for instructions, and instructions from other devices arrive through that connection. The other devices never connect to this machine directly, and it works with outbound HTTPS traffic only, so you do not need to open any ports on your router.
To use it, you must be logged in to Claude Code with an account on a Pro, Max, Ultra, Team, or Enterprise plan (on Team and Enterprise, an administrator must enable Remote Control first). It is not available if you use Claude Code with an API key.
With the default --spawn same-dir, all sessions run in the same folder, so you need to avoid editing the same file from multiple sessions at once. If you want a separate git worktree for each session, use --spawn worktree. A process started over SSH ends when the connection is closed, so I start it inside tmux to keep it running.
Working Folder Layout
I start Claude Code in a single working folder that holds the repositories of all the sites. The working folder itself is a git repository, and each site's repository and the nginx configuration repository are included in it as submodules. With the folder names replaced by examples, the layout looks like this.
workspace/ ← folder where Claude Code is started
├── CLAUDE.md ← constraints and setup shared by all sites
├── .claude/
│ ├── settings.json ← permissions when started in this folder
│ ├── skills/ ← procedures for each task (skills)
│ └── rules/ ← rules loaded only when matching files are opened
├── docs/ ← change log and open issues
├── memory/ ← Claude Code's memory
├── site-blog/ ← one repository per site
│ └── CLAUDE.md ← commands and cautions for that site
├── site-tool/
│ └── CLAUDE.md
└── proxy/ ← nginx configurationEach site's source is served by sharing that site's folder directly with its Docker container (a bind mount), so when Claude Code edits a file, the file inside the production container changes at the same moment. A Next.js site does not change until it is rebuilt, but a process that watches for file changes and restarts automatically restarts as soon as the file is saved.
Verifying Changes End to End with Playwright
The server has Playwright, a tool for controlling a browser from a program, and Chromium installed. After Claude Code makes a change, the verification is completed on this machine, up to an end-to-end (E2E) check that opens the page, operates it, and confirms the result. Screenshots for articles are taken the same way.
Builds for verification are done after copying the folder to /tmp, so they do not overwrite the build output being served. When I only need to check the screen, I sometimes have Claude Code start a development server on a port separate from production and open it there.
When Claude Code opens my own sites automatically, it opens them with ads and analytics turned off, so that pages opened for verification are not counted as ad impressions or in analytics.
What I Let Claude Code Do
For each type of work, I separate what Claude Code may do without confirmation from what requires my confirmation before it runs.
Work | Without confirmation | Requires my confirmation |
|---|---|---|
Code changes | Editing, type checks, linting | Production builds and deployment |
Articles | Outlines, drafts, diagrams | Draft content, publishing, paid image generation |
Investigation | Reading logs, analytics, and the database | — |
Verification | E2E checks and screenshots with Playwright | Checks with ads displayed |
Server administration | Checking the state of Docker containers and processes | Operations that affect production (see below) |
git | — | Commit and push at the end of a unit of work |
Splitting CLAUDE.md into Three Layers and Four Kinds of Content
Looking back at the incidents in production, most were caused not by a wrong command but by Claude Code not having the information about what that command does in this environment. For example, running a type-check command once made pnpm detect a dependency mismatch in its pre-run check and start reinstalling the production dependencies. Another time, editing .env and rebuilding was not enough, and the site went live with the old environment variables that had been fixed when the Docker container was created. Environment-specific behavior like this can only be passed on to Claude Code by writing it in CLAUDE.md or rule files.
Claude Code loads the user-wide ~/.claude/CLAUDE.md and the CLAUDE.md of the folder it starts in at the beginning of a session, and it loads a subfolder's CLAUDE.md when it opens a file in that subfolder. I follow this behavior and split CLAUDE.md into three layers.
Layer | Location | What I write there |
|---|---|---|
User-wide |
| Response language, how to ask for confirmation, asking about commit and push at the end of a unit of work |
Working folder | Root of the working folder | That this is production and which operations need confirmation, the server layout, rules shared by all sites |
Per site | Root of each repository | That site's commands, where the source of truth for data and settings lives, cautions specific to that site |
Limiting CLAUDE.md to Four Kinds of Content
At one point, the CLAUDE.md files added up to 2,090 lines. The main reason was that I had been adding background and measurements to CLAUDE.md after every task. So I limited CLAUDE.md to the following four kinds of content and reviewed every file, bringing the total down to 940 lines.
- Constraints that cause incidents if broken, with one line on what happens
- Where the source of truth for data, settings, and generated files lives
- Procedures and commands used every time
- Pointers to detailed documents, with when to read them
Background, measurements, and ideas that did not work go into the change log, and unresolved problems go into the open issues file. CLAUDE.md itself also says to keep each item within three lines and, when in doubt, to decide by asking whether the item is needed in every session.
Rules That Load Only When a Matching File Is Opened
Rules that are needed only when touching specific files, such as the procedure for changing the nginx configuration, go in the working folder's .claude/rules/. If you write paths at the top of the file, the rule is loaded only when a file matching the pattern is opened.
---
paths:
- "proxy/conf.d/**"
- "proxy/nginx.template.conf"
---
# Read before changing the nginx configuration
- nginx.conf is a generated file, so do not edit it directly. Edit the template and regenerate it
- Ask for confirmation before reloading the configurationA rule is loaded when Claude Code handles a matching file with its Read, Write, or Edit tool, and when it reads a single matching file with cat or sed in Bash. When I tried it in my environment, piping the output of cat into head did not load the rule. If a rule's body pulls in another file with @, that part is loaded at the start of the session, before any file is opened, so I write content I want loaded later directly in the rule body.
I move rules here that are not needed every time but cause incidents if overlooked. However, since the trigger is limited to opening a file, cautions for operations that do not involve opening a file, such as restarting a Docker container, stay in the main CLAUDE.md.
Making Claude Code Ask Before Risky Operations
Prohibitions written in CLAUDE.md are not always followed. So besides listing the operations that need confirmation in CLAUDE.md, I define how confirmation is requested and re-inject those rules with hooks every time.
Operations That Always Need Confirmation
At the top of the working folder's CLAUDE.md, I wrote that the following operations always require confirmation before they run.
- Stopping, recreating, or restarting Docker containers
- Restarting, stopping, or changing the number of processes
- Applying database migrations
- Deleting files, volumes, or images
- Changing and reloading the nginx configuration, replacing SSL certificates
- git operations that rewrite the working folder (
checkout,reset,pull,clean)
I also state explicitly that read-only operations, such as viewing logs or process status, do not need confirmation. Being asked for confirmation every time gets tiresome, so I limit confirmation to operations that affect production.
Asking with a Recommended Option
How confirmation is requested is defined in the user-wide CLAUDE.md. Claude Code has a feature for asking questions with options (AskUserQuestion), and I instruct it to put the recommended option first and add one line of reasoning to each option.
Because the question comes with options, I can answer with a single choice if the recommendation is fine, and if none of them fit, I can write a free-form answer under "Other". Claude Code asks in these situations: commit and push, decisions that are hard to reverse later such as design direction, cases where the specification can be read in more than one way, and deleting files or deploying to production.
Confirming Commit and Push Once per Unit of Work
I used to have a hook ask for permission on every commit and push. Being asked every time became tiresome, so I removed it. Now commit and push can run without a permission prompt, and Claude Code asks about commit and push together, once, when a unit of work is finished.
If Claude Code tries to end its response without asking, a Stop hook (a hook that runs just before Claude Code finishes its response) checks for uncommitted or unpushed changes and stops it. It does not stop twice for the same state, so it does not keep stopping on changes I have answered "do not commit" for.
The git status that this hook runs rewrites git's index. Because of that, a process that watches for file changes and restarts automatically was restarted every time the hook ran. Now the hook runs git with GIT_OPTIONAL_LOCKS=0 so that it does not rewrite the index, and .git is also excluded from the watch targets.
Hooks and Permission Settings
Rules I always want followed, such as how to ask for confirmation, are kept in one file, and a UserPromptSubmit hook (a hook that runs every time I send a message) attaches them to every message. When I edit the rule file, the change applies from the next message. The confirmation-related part of the user-wide settings (~/.claude/settings.json) looks like this.
{
"permissions": {
"allow": ["Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)"]
},
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "$HOME/.claude/hooks/inject-rules.sh", "timeout": 10 }] }
],
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "$HOME/.claude/hooks/guard-production.py", "timeout": 10 }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "$HOME/.claude/hooks/check-git.sh", "timeout": 15 }] }
]
}
}Operations allowed without confirmation are split between the user-wide settings and the working folder's settings (.claude/settings.json). High-impact ones, such as connecting to an external server, are allowed without confirmation only in sessions started in the working folder.
All of this is for making Claude Code ask for confirmation, so if the instructions are not followed, commands run without it. For that reason, a PreToolUse hook, which runs before a command is executed, checks for commands that affect production and shows a permission prompt when one matches. It covers stopping or restarting Docker containers, stopping processes, migrations, reloading the nginx configuration, git operations that rewrite the working folder, and deleting build output, including commands run inside docker exec or over SSH. The hook does not deny anything; it only shows the prompt, so the command runs once I approve it. Because it decides from the command string, it can miss cases, so the instructions in CLAUDE.md and the AskUserQuestion confirmations stay in place.
Restricting Permissions for Unattended Claude Code Runs
I used to run Claude Code unattended every morning with cron (claude -p) to research article topics and leave material for writing in a folder. The suggested topics were not good enough, so I have stopped it. I now choose topics myself.
When I built it, I found that unattended runs load the same user and project settings as interactive use. As a result, the permission to commit and push without confirmation, along with the Stop hook, carried over to the unattended runs. Because unattended Claude Code reads external web pages, if it had followed instructions written on a page (prompt injection), it could have used those permissions.
So for unattended runs, I pass a separate, narrower configuration instead of the interactive one.
--restrictedkeeps user and project settings from loading and removes the tools that run commands (tools named with--toolsremain available)- A dedicated settings file (
--settings) limits writes to one working folder and command execution to my own scripts --permission-mode dontAskdenies every operation that would need confirmation unless it is on the allow list (reads still go through, so files containing secrets are blocked withdeny)- Auto memory (a feature where Claude Code writes down what it learned in files and loads them in the next session) is turned off. The storage location is determined per git repository, so leaving it on would let the run write to the interactive memory
- The database and git are handled by preprocessing scripts, and Claude Code only reads the resulting files
claude -p --restricted \
--tools Read Glob Grep Write Edit WebFetch WebSearch Bash \
--settings ./config/headless-settings.json \
--permission-mode dontAsk \
--output-format json < prompt.mdAfter settling the configuration, I used dummy files to confirm that writing outside the working folder, reading files that contain secrets, and running git are all denied.
Carrying Knowledge Across Sessions
A new session does not contain the conversation from the previous one. Before finishing its work, Claude Code writes the information needed in the next session to files, split into four places depending on the content.
Where | What | Example |
|---|---|---|
CLAUDE.md | Constraints, sources of truth, and procedures needed every time | Do not delete the production build output |
Change log | Background, measurements, ideas that did not work | Why a setting was chosen and what was measured at the time |
Open issues | Problems not fixed yet and planned checks | The date to check whether a fix worked |
Memory | My preferences and ongoing projects | Write without metaphors |
For memory, I use the auto memory feature, with its storage location moved into the working folder with the autoMemoryDirectory setting and managed with git.
When a task is done, Claude Code writes what it learned into these categories and lists the updated files in its completion report. When a session starts, a SessionStart hook shows failed backups and items awaiting confirmation.
Putting Repeated Procedures into Skills
I keep the procedures for repeated work in skills (files that bundle procedures and reference material, which Claude Code loads when needed) instead of CLAUDE.md. At session start, only each skill's description (description) is loaded, and the body loads only when the skill is used, so long procedures do not add to the context of other work. The official documentation also suggests creating a skill when a section of CLAUDE.md has grown into a procedure rather than a fact.
The Three Skills I Wrote
I wrote the following three skills. The article-writing skill lives in .claude/skills/ inside the blog repository, and the other two live in the working folder's .claude/skills/.
Skill | What the procedure includes | Why it is a skill |
|---|---|---|
Article writing | The steps from choosing a topic to publishing, style rules, a style checker, and the script for posting to the CMS | The rules and steps are long and needed only when writing articles |
Analytics and ad revenue reports | How to fetch the data, how to use the reporting scripts, and numbers that are easy to misread | The steps and cautions were scattered across memory, and the scripts were copied each time |
Security audits | How to read the results collected by the cron script, how to judge vulnerabilities, and how to apply updates | To keep judging results and deciding on updates out of unattended runs |
A skill in a subfolder is not loaded until Claude Code reads or edits a file in that folder, so the article-writing skill does not appear in the skill list right after starting in the working folder.
I wrote the reporting skill after running the same report 16 times. Cautions such as not treating the last two days of Google Search Console data as final were scattered across the memory entries for each run, and I copied the reporting scripts from the previous run's working directory each time. So I put the steps, the cautions, and how to use the scripts into one skill.
The security audit skill splits its role with a cron script. Logs and vulnerability advisories contain strings that an attacker can control, so unattended Claude Code does not read them, and the cron script only collects information and compares it with a baseline. Judging the results and deciding on updates happen in a session I open, following the skill's procedure, and updates run only after I approve them. Because the checks run on the server being examined, an attacker with administrator privileges could hide their traces. So reports stop at "no signs found in the checked range" and never say "not compromised".
What to Write in the Description and at the Top of the Body
Claude Code reads a skill's description to decide whether to use it. In the description, I write the situations in which to use the skill rather than what the skill contains. The reporting skill's description also lists how requests are phrased, such as "show me the revenue" and "why did it drop".
When the context is compacted, only the first 5,000 tokens of each invoked skill's body are loaded again. So I put the easy-to-get-wrong points at the top of SKILL.md, split the detailed rules into separate files, and write in SKILL.md to reread those files after compaction.
Applying Updates to Published Skills Only After Checking Their Content
Besides my own skills, I install skills published by shadcn/ui, Payload, Prisma, and Vercel in the user-wide ~/.claude/skills/ with the skills command. My sites are self-hosted in Docker containers, so I have not installed the skills for deploying to Vercel or for Prisma's cloud products.
A skill's body is loaded as instructions to Claude Code. Anthropic's documentation warns to use only skills you created yourself or obtained from Anthropic, and to audit anything from another source thoroughly before use.
This server holds the production source and files containing secrets, so taking a provider's changes as they are could run instructions slipped into those changes with access to those files. For that reason, updates are not applied automatically. Once a week, a cron script tries the update with HOME pointed at a separate folder and writes any differences to a report file. CLAUDE.md says to tell me at session start if that report exists. I apply updates after checking the differences. Tools listed in allowed-tools can be used without a permission prompt during the turn that invokes the skill, so I also check for changes in that field.
Writing Articles and Creating Images with a Skill
Recent articles on this site are written by Claude Code following procedures kept in a skill. The skill covers everything from choosing a topic to style rules, article structure, image creation, and posting to the CMS. Three of these steps directly affect the content and look of an article, so they are always performed.
Interviewing Me Before Writing
The skill includes a step where, after the outline is set and before the body is written, Claude Code asks me only about points it cannot find out by researching. It lists the points readers are likely to be unsure about from the top-ranking competing articles, checks what it can learn from official documentation, the code, and the server's records on its own, and then asks only about the remaining points where my experience or judgment would change the text. I do not let it write about anything I have not experienced, so without facts only I have, such as measurements, failures, and the reasons for my choices, the article would be a generic piece anyone could have written.
I used to have it ask the same six questions for every article, such as where I failed first and which options I dropped along the way. In how-to and comparison articles, questions unrelated to the topic remained, their answers ended up in the text, and I had more to reread, so I stopped fixing the number of questions. Now each question must name the heading its answer would go under, and questions that cannot name one are not asked. Answers left out of the text are reported along with the draft, with the reason.
Comparing the Writing Style with My Articles by the Numbers
The style rules are based on figures measured from 14 articles I wrote myself in Japanese. For example, my sentences average 52 characters, while the articles Claude Code wrote when the skill was first created averaged 31.7 characters, with short sentences of similar length lined up one after another.
A bundled checker compares each draft with my articles on sentence-length variation and how often Japanese quotation brackets (「」) and the word for "I" appear, and it also checks for words I do not use. I read the draft only after every item is within range. Places where I think "I would not write it this way" are added to a "written like this / write it like this" table.
Even so, when I checked 12 articles written by Claude Code again, I found 18 factual errors and descriptions that had become outdated after the tools changed. The outdated descriptions were cases where I changed a tool's behavior after the article was written but did not update the article. Now, opening a file that changes a tool's screens or limits loads a rule that asks for the article describing that behavior to be updated as well.
Drawing Images as SVG and Checking Them at Small Size
Thumbnails and diagrams are drawn by Claude Code as SVG and converted to PNG or WebP with a small tool I wrote. This server has no Japanese fonts installed, so converting as-is turns every Japanese character into "□"; the tool bundles Noto Sans JP and loads it only during conversion.
Claude Code can load the converted image and check its content, so I have it also look at the thumbnail scaled down to the size shown in the article list and confirm that the text is readable.
Only when a photo-like image is needed do I use a separate tool that calls the API of Google's image generation model, Nano Banana Pro. It charges per image, so Claude Code shows a cost estimate before running it and generates the image only after I confirm.
Splitting Research and Review across Subagents
Research and reviews are split across subagents (Claude Code instances that run in a separate context) and run in parallel.
Choosing Models for Subagents
I am on Claude's Ultra plan. I use Claude Code on the same account from morning to night, both for running this server and for developing other projects. Recently I have been using the workflow feature, which runs multiple subagents in a fixed sequence on large codebases, more often.
Once, I started six subagents at the same time without specifying a model, and they ran on Fable, the same model as the parent session, using up a large share of my usage. Fable is the model with the highest price per token. Since then, I let Claude Code choose the model, but it must get my approval before using Fable.
The model that an alias such as sonnet points to is determined by the Claude Code version. In my environment, automatic updates for Claude Code were turned off, so even after a new Sonnet was released, subagents that specified sonnet kept running on the previous Sonnet. The resident Remote Control process also keeps running on the version it was started with, so after updating Claude Code, the resident process needs to be restarted as well. A cron script now checks once a week whether a new version is out.
Having Another Subagent Review My Own Diff
After fixing a batch of bugs, I have another subagent review the diff of those fixes once more. When I fixed 59 bugs found in a review of one of my tool sites at once, this diff review found 6 new bugs introduced by the fixes. All of them had passed type checks and linting. Two of them recreated, in a different form, the problem the fix was meant to solve.
The diff review happens before the fixes are committed. Claude Code pulls the pre-fix files with git show HEAD:<path> and compares them to confirm that the diff really introduced each problem.
Summary
Most incidents on the production server were caused by environment-specific behavior that had not been passed on to Claude Code. Each time one happened, I added a rule: I write it in CLAUDE.md, define how confirmation is requested, and re-inject those rules with hooks every time.
- Split CLAUDE.md into three layers (user-wide, working folder, per site) and limit it to four kinds of content: constraints, sources of truth, procedures, and pointers
- Ask for confirmation only for operations that affect production, using options with a recommendation
- For unattended runs, do not load the interactive settings; pass a separate, narrower configuration
- Have Claude Code write background to the change log and preferences and ongoing projects to memory, and carry them over to the next session
- Put repeated work such as article writing into skills, and apply updates to published skills only after checking their content
Related articles
This guide explains how to deploy multiple Next.js web applications on the same server using reverse proxy functionality, with Open LiteSpeed as the reverse proxy solution.
This guide explains how to deploy web applications and services developed with Next.js to on-premise environments like VPS servers or self-hosted systems. While this approach requires more manual effort compared to using Vercel, it offers significant cost savings and greater operational flexibility.
This guide explains how to directly connect a local server running at home or a Linux OS container on a virtual machine to PPPoE and assign a fixed global IPv4 address.
