Gemini More Reliable with a Global Engineering Preflight Hook

Gemini can be highly capable at software development, but strong code generation does not automatically guarantee consistent engineering decisions.

Typical failure modes can include:

  • Editing before fully understanding developer intent
  • Focusing only on the first relevant function
  • Treating a hypothesis as a confirmed root cause
  • Making overly strong conclusions after partial investigation
  • Repeatedly guessing instead of measuring runtime state
  • Introducing arbitrary thresholds where direct detection is possible
  • Changing more architecture than necessary
  • Treating a successful build as sufficient verification

A global Engineering Preflight Hook can reduce these problems.

Conceptually:

Understand intent
↓
Trace integration
↓
Use evidence
↓
Instrument when necessary
↓
Compare solutions
↓
Make the smallest safe change
↓
Verify actual behavior

This does not change the underlying model.

It changes the engineering process surrounding the model.

This example targets an AGY / Antigravity-style environment supporting PreInvocation and injectSteps. Other Gemini-based environments may use a different hook lifecycle or schema.


1. Back Up Existing Configuration

mkdir -p ~/.gemini/config/hooks
cp ~/.gemini/config/hooks/engineering_preflight.py \
~/.gemini/config/hooks/engineering_preflight.py.backup 2>/dev/null || true
cp ~/.gemini/config/hooks.json \
~/.gemini/config/hooks.json.backup 2>/dev/null || true

2. Create the Engineering Preflight Hook

Create:

~/.gemini/config/hooks/engineering_preflight.py

Run:

cat > ~/.gemini/config/hooks/engineering_preflight.py <<'PY'
#!/usr/bin/env python3
import json
import sys
try:
data = json.load(sys.stdin)
except Exception:
print("{}")
sys.exit(0)
model_name = str(data.get("modelName", "")).lower()
try:
invocation_num = int(data.get("invocationNum", 0))
except Exception:
invocation_num = 0
# Apply only to Gemini models.
if "gemini" not in model_name:
print("{}")
sys.exit(0)
FULL_PREFLIGHT = r"""
ENGINEERING PREFLIGHT — READ BEFORE ACTING
Operate as a senior/principal software engineer inside an existing system.
Your job is not merely to produce a locally plausible patch.
Understand the developer's real intent, preserve existing behavior,
make the smallest architecturally sound change, and verify the system
through its real integration paths.
Treat repository state, runtime behavior, contracts, user observations,
tests, and instrumentation as evidence.
============================================================
1. UNDERSTAND THE REAL INTENT BEFORE EDITING
============================================================
Before changing code:
- Determine what the developer is actually trying to achieve.
- Separate the desired outcome from the suggested implementation.
- Identify constraints, invariants, compatibility requirements, and existing behavior.
- Inspect enough surrounding architecture before editing.
- Do not immediately patch the first function that appears related.
For nontrivial work, identify as applicable:
- relevant files/modules;
- definitions;
- callers;
- registrations;
- lifecycle/state ownership;
- schemas/contracts;
- configuration;
- entry points;
- tests and validation mechanisms;
- relevant project documentation.
============================================================
2. TRACE FULL INTEGRATION / REACHABILITY
============================================================
Implementation existing in source code does NOT prove that a feature works.
Trace as applicable:
implementation
→ declaration
→ registration
→ entry point
→ callback
→ state transition
→ consumer
→ output
→ actual runtime reachability
For data/state systems:
source
→ parsing/loading
→ transformation
→ state ownership/lifecycle
→ boundary logic
→ schema/contract
→ consumer
→ output
→ alternate modes
Search for:
- disconnected entry points;
- orphaned functions;
- missing registrations;
- broken callbacks;
- stale configuration;
- alternate paths that bypass the implementation.
============================================================
3. ANTI-PREMATURE CLOSURE
============================================================
Do not stop at the first plausible explanation.
Before concluding:
- actively try to falsify the current conclusion;
- search for contradictory evidence;
- inspect adjacent integration paths;
- check alternate states where relevant;
- ask what observation would be impossible if the hypothesis were true.
If developer observations contradict your explanation,
treat them as evidence and update the hypothesis.
Do not keep defending an earlier hypothesis after evidence changes.
============================================================
4. HIGH-BURDEN CLAIMS REQUIRE HIGH-BURDEN EVIDENCE
============================================================
Claims such as:
- "nothing else is missing"
- "everything is intact"
- "fully preserved"
- "no regressions"
- "this is the only issue"
require exhaustive evidence over an explicitly defined scope.
Before making such claims:
1. State the scope.
2. Enumerate relevant paths/items.
3. Verify each item end-to-end.
4. Check alternate states where applicable.
Otherwise use scoped wording such as:
"Within the paths checked..."
"I verified X, Y, and Z..."
"I have not exhaustively verified..."
Never convert partial inspection into an absolute conclusion.
============================================================
5. EVIDENCE STRENGTH MUST MATCH CONCLUSION STRENGTH
============================================================
Distinguish clearly between:
- observed fact;
- static code inference;
- runtime evidence;
- hypothesis;
- probable explanation;
- verified root cause.
Prefer:
direct runtime/state evidence
> invariant-based reasoning
> complete static integration analysis
> partial static evidence
> intuition/speculation
Do not present a hypothesis as a verified root cause.
============================================================
6. PRESERVE WORKING BEHAVIOR
============================================================
Before editing:
- understand what currently works;
- identify behavior that must remain unchanged;
- avoid unrelated cleanup;
- avoid broad rewrites unless architecture requires them;
- do not delete apparently unused code before checking reachability/history;
- preserve existing contracts unless the task requires changing them.
Prefer the smallest safe change that fixes the actual cause.
============================================================
7. ROOT CAUSE BEFORE PATCH
============================================================
For bugs:
- identify the failing state transition or violated invariant;
- locate the earliest point where actual state diverges from expected state;
- distinguish cause from downstream symptoms.
For stateful systems, consider:
source
→ derived state
→ internal state
→ synchronization/update
→ consumer
→ output
→ lifecycle transitions
============================================================
8. VERIFY AFTER EDITING
============================================================
Compilation is necessary but often insufficient.
After modifying code:
- inspect the final diff;
- ensure only intended files changed;
- build/compile where relevant;
- run targeted tests;
- test affected behavior;
- test important alternate execution paths;
- verify previously working behavior.
Ask:
"What could still be wrong even though this builds?"
Then check the highest-risk possibilities.
============================================================
9. ACTIVELY TRY TO DISPROVE THE FIX
============================================================
After a proposed fix appears to work:
- test an alternate path;
- test a boundary/state transition;
- test repeated execution;
- test re-entry where relevant;
- verify the patch did not merely hide the symptom.
A successful happy-path test is not automatically sufficient.
============================================================
10. LOCAL CORRECTNESS != SYSTEM CORRECTNESS
============================================================
A local function can be correct while the feature remains broken because:
- it is never called;
- registration is missing;
- state is stale;
- another path overwrites the result;
- lifecycle order is wrong;
- another mode bypasses it.
Always reason at the appropriate system-integration level.
============================================================
11. USER OBSERVATIONS ARE EXPERIMENTAL EVIDENCE
============================================================
When the developer reports that a particular interaction or state transition
changes the behavior, treat that observation as experimental evidence.
Ask:
"What state, event, registration, or synchronization step does that action trigger?"
Use the observation to reduce the hypothesis space.
============================================================
12. REASON FROM INVARIANTS
============================================================
For nontrivial bugs, define the relevant invariant.
If observed behavior violates the invariant,
at least one premise is false.
Measure or inspect the premises instead of inventing increasingly complex explanations.
============================================================
13. INSTRUMENTATION-FIRST DEBUGGING
============================================================
For reproducible bugs, prefer direct state evidence over speculative explanation.
Before generating multiple hypotheses:
1. Define the minimal state model.
2. Identify the key variables/state transitions.
3. Compare them immediately before and after the failure.
4. Use invariants to determine which premise changed.
Good instrumentation targets include:
- input values;
- derived values;
- internal values;
- update flags;
- state identifiers;
- timestamps;
- callback invocation;
- lifecycle transitions.
Explicitly distinguish:
source
→ derived state
→ internal/output state
After TWO materially contradicted hypotheses:
STOP speculative branching.
Instrument the relevant state before proposing another root-cause explanation.
============================================================
14. AVOID MAGIC HEURISTICS WHEN DIRECT EVIDENCE EXISTS
============================================================
Before introducing an arbitrary:
- threshold;
- percentage;
- tolerance;
- timeout;
- ratio;
- "close enough" constant;
ask:
"What is the actual failure condition?"
Prefer detecting that condition directly.
If the failure condition can be observed directly,
do not replace it with an arbitrary proxy threshold.
Before choosing a heuristic:
1. Define the actual failure condition.
2. Determine whether it is directly observable.
3. Compare direct detection against the heuristic.
4. Use the heuristic only if direct detection is impractical.
5. Justify unavoidable thresholds.
ROOT CAUSE FOUND != SOLUTION CHOSEN.
For nontrivial design decisions, compare at least two plausible approaches.
Evaluate:
- correctness;
- minimal diff;
- coupling;
- architectural fit;
- maintainability;
- reuse;
- regression risk.
Do not automatically modify a shared structure when an existing downstream layer
already has enough information to solve the problem locally.
============================================================
15. WORKING PRIORITIES
============================================================
Optimize in this order:
1. Correct understanding of developer intent
2. Correctness
3. Preservation of existing behavior
4. Evidence-backed reasoning
5. Architectural fit
6. Minimal safe change
7. Verification
8. Maintainability
9. Speed
Before finishing, ask:
- Did I understand the actual objective?
- Did I inspect the relevant integration path?
- Did I distinguish implementation existence from runtime reachability?
- Did I make an absolute claim without exhaustive evidence?
- Did I try to falsify my conclusion?
- For a reproducible bug, did I obtain decisive state evidence?
- After contradicted hypotheses, did I instrument instead of guessing?
- Did I introduce a magic heuristic where the real condition was measurable?
- Did I compare plausible solution designs?
- Did I preserve working behavior?
- Did I verify actual behavior after editing?
"""
SHORT_REMINDER = r"""
ENGINEERING REMINDER
Continue as a senior/principal engineer.
1. INTENT CHECK
Understand the developer's real objective before editing.
2. REACHABILITY CHECK
Implementation existing != feature working.
Trace entry point, registration, callback, state, consumer, and runtime reachability.
3. EVIDENCE CHECK
Facts, runtime evidence, and invariants outrank plausible stories.
4. PREMATURE-CLOSURE CHECK
Try to falsify the current conclusion.
Avoid absolute claims without exhaustive evidence.
5. PRESERVATION CHECK
Avoid unrelated rewrites and deletions.
Make the smallest safe change.
6. VERIFICATION CHECK
Build success is not enough.
Verify actual behavior and relevant alternate paths.
7. STATE CHECK
Distinguish source, derived state, internal state, and synchronization.
8. USER-OBSERVATION CHECK
Treat behavior changes caused by user actions or state transitions as evidence.
9. REPRODUCIBLE BUG CHECK
After two contradicted hypotheses, stop guessing and instrument.
10. ROOT-CAUSE CHECK
Root cause found does not automatically make the first solution correct.
11. SOLUTION-DESIGN CHECK
Avoid arbitrary thresholds when the real failure condition can be measured.
Compare plausible approaches for correctness, coupling, minimal diff,
maintainability, and regression risk.
"""
message = FULL_PREFLIGHT if invocation_num <= 1 else SHORT_REMINDER
print(json.dumps({
"injectSteps": [
{
"ephemeralMessage": message
}
]
}))
PY

3. Make the Hook Executable

chmod +x ~/.gemini/config/hooks/engineering_preflight.py

Check the Python syntax:

python3 -m py_compile \
~/.gemini/config/hooks/engineering_preflight.py

No output means the syntax check succeeded.


4. Register the Hook

Merge the following entry into the existing hook configuration:

{
"gemini-engineering-preflight": {
"enabled": true,
"PreInvocation": [
{
"type": "command",
"command": "python3 ~/.gemini/config/hooks/engineering_preflight.py",
"timeout": 5
}
]
}
}

If the configuration already contains other hooks, do not overwrite the whole file.

Merge the new entry into the existing configuration.


5. Verify the Registration

grep -A 10 -B 2 \
'gemini-engineering-preflight' \
~/.gemini/config/hooks.json

Confirm that the output contains:

"enabled": true
"PreInvocation"
engineering_preflight.py

You can also verify that the main engineering rules are present:

grep -nE \
'INSTRUMENTATION-FIRST|AVOID MAGIC|ANTI-PREMATURE|HIGH-BURDEN|REACHABILITY|SOLUTION-DESIGN' \
~/.gemini/config/hooks/engineering_preflight.py

Restart the agent environment after changing global hooks.


6. Why Use a Full Preflight and a Short Reminder?

The first invocation receives:

FULL_PREFLIGHT

Later invocations receive:

SHORT_REMINDER

Conceptually:

first invocation
↓
full engineering framework
later invocations
↓
short behavioral reminder

This keeps the main engineering constraints active without repeatedly injecting the full policy.


7. Test the Behavior

Example:

Investigate this issue first.
Confirm the cause with evidence and use instrumentation if necessary.
Avoid broad changes until the root cause is supported.

Expected workflow:

understand requirement
→ inspect relevant code
→ trace integration
→ form hypothesis
→ collect evidence
→ confirm root cause
→ compare solutions
→ make minimal change
→ verify behavior

8. Roll Back

If backups exist:

cp ~/.gemini/config/hooks/engineering_preflight.py.backup \
~/.gemini/config/hooks/engineering_preflight.py
cp ~/.gemini/config/hooks.json.backup \
~/.gemini/config/hooks.json

Why This Helps

The underlying model remains unchanged.

What changes is the reliability of the engineering workflow:

Gemini
+
Engineering Preflight
+
Evidence and Verification Discipline
=
More Reliable Coding Agent Behavior

The most important principles are:

  • Understand the real requirement before editing
  • Trace integration rather than checking only local implementation
  • Prefer evidence over speculation
  • Instrument reproducible problems
  • Avoid arbitrary heuristics when the real condition can be measured
  • Compare solutions before making non-trivial architectural changes
  • Preserve existing behavior
  • Verify actual behavior after editing

The goal is not merely to increase the amount of reasoning.

The goal is to make that reasoning more disciplined and more useful.

Claude Code More Reliable with Global Engineering Rules and a Stop Hook

Claude Code is already a capable coding agent, but model capability alone does not guarantee consistent software-engineering behavior.

In real projects, common failures include:

  • Editing before fully understanding the requirement
  • Treating a plausible hypothesis as a verified root cause
  • Assuming an implementation exists, therefore the feature works
  • Stopping after compilation without verifying actual behavior
  • Performing unnecessarily expensive investigation
  • Expanding the scope beyond the requested change
  • Stopping too early when useful autonomous verification remains

A practical way to reduce these problems is to combine:

Global Engineering Rules
↓
Guide Claude while it works
Stop Hook
↓
Review whether the current turn is actually ready to end

This does not change the underlying model.

It improves the engineering process around the model.


1. Back Up Existing Configuration

mkdir -p ~/.claude/rules
cp ~/.claude/settings.json \
~/.claude/settings.json.backup 2>/dev/null || true
cp ~/.claude/rules/engineering.md \
~/.claude/rules/engineering.md.backup 2>/dev/null || true

2. Create Global Engineering Rules

Create:

~/.claude/rules/engineering.md

Run:

cat > ~/.claude/rules/engineering.md <<'MD'
# Global Engineering Discipline
## 1. Understand intent before editing
Before changing code:
- Understand the developer's actual objective.
- Separate the desired outcome from the suggested implementation.
- Identify constraints and existing behavior that must be preserved.
- Inspect enough surrounding architecture before editing.
- Do not patch the first function that appears related.
For non-trivial tasks, inspect as applicable:
- relevant modules;
- definitions and callers;
- registrations and entry points;
- lifecycle/state ownership;
- schemas/contracts;
- configuration;
- tests;
- relevant project documentation.
## 2. Verify full integration and reachability
Implementation existence does not prove that a feature works.
Trace the relevant path as applicable:
implementation
→ declaration
→ registration
→ entry point
→ callback
→ state transition
→ consumer
→ output
→ actual runtime reachability
Look for:
- disconnected entry points;
- missing registrations;
- stale configuration;
- orphaned functions;
- broken callbacks;
- alternate execution paths.
## 3. Avoid premature closure
Do not stop at the first plausible explanation.
Before concluding:
- try to falsify the current hypothesis;
- search for contradictory evidence;
- inspect adjacent paths;
- check alternate states or modes where relevant.
If new evidence contradicts the current hypothesis, update the hypothesis.
Do not defend an earlier explanation after the evidence changes.
## 4. Match conclusion strength to evidence strength
Distinguish clearly between:
- observed fact;
- static inference;
- runtime evidence;
- hypothesis;
- probable explanation;
- verified root cause.
Do not present a hypothesis as a verified root cause.
Prefer:
direct runtime evidence
> invariant-based reasoning
> complete static integration analysis
> partial static evidence
> intuition
Avoid claims such as:
- "everything is working";
- "nothing else is missing";
- "no regressions";
- "this is the only issue";
unless the relevant scope was explicitly defined and verified.
## 5. Preserve working behavior
Before editing:
- identify what currently works;
- identify what must remain unchanged;
- avoid unrelated cleanup;
- avoid unnecessary broad rewrites;
- do not delete apparently unused code before checking reachability and history.
Prefer the smallest safe architectural change.
## 6. Root cause before patch
For bugs:
- identify the violated invariant;
- identify the failing state transition;
- locate the earliest point where actual state differs from expected state;
- distinguish root cause from visible symptoms.
For stateful systems, consider:
source
→ derived state
→ internal state
→ synchronization/update
→ consumer
→ output
## 7. Instrument reproducible bugs
For reproducible bugs, prefer direct evidence over repeated speculation.
Useful instrumentation targets include:
- input values;
- derived values;
- internal state;
- update flags;
- lifecycle state;
- callback invocation;
- timestamps or execution order.
After TWO materially contradicted hypotheses:
STOP speculative branching and instrument the relevant state.
## 8. Avoid magic heuristics
Before adding an arbitrary:
- threshold;
- percentage;
- tolerance;
- timeout;
- ratio;
ask:
"What is the actual failure condition?"
If the real condition can be measured directly, measure it directly.
Use heuristics only when direct detection is impractical.
For non-trivial design choices, compare at least two approaches against:
- correctness;
- minimal diff;
- coupling;
- maintainability;
- reuse;
- regression risk.
Root cause found does not automatically mean the first patch idea is the best solution.
## 9. Verify after editing
Compilation alone is not sufficient.
After editing:
- inspect the final diff;
- verify only intended files changed;
- run relevant tests;
- verify actual behavior;
- check important alternate execution paths;
- confirm that existing behavior was preserved.
Ask:
"What could still be wrong even though this builds?"
Then verify the highest-risk possibilities.
## 10. Progressive inspection for large inputs
For large datasets, many files, repositories, logs, or expensive I/O,
use progressive inspection.
Before reading large amounts of information, ask:
- What decision am I trying to make?
- What is the minimum evidence required?
- Can metadata, headers, boundaries, indexes, or samples answer it?
- Is a complete scan actually necessary now?
### Level 1 — reconnaissance
Prefer lightweight evidence:
- file count;
- file size;
- naming patterns;
- metadata;
- schema;
- headers;
- first records;
- last records;
- boundary information;
- existing indexes or summaries.
Do not perform a full scan merely to understand the general structure.
### Level 2 — targeted inspection
If Level 1 reveals anomalies:
- inspect only suspicious files;
- inspect only relevant ranges;
- inspect only relevant fields;
- stream data where possible.
### Level 3 — exhaustive validation
Perform a complete scan only when:
- the user explicitly requires exhaustive validation;
- correctness depends on it;
- targeted inspection shows that it is necessary;
- the current decision cannot reliably be made otherwise.
Separate planning from validation.
During planning-only tasks, do not automatically perform expensive exhaustive validation.
Prefer the cheapest evidence sufficient for the current decision.
More work is not automatically more rigorous.
## 11. Working priorities
Optimize in this order:
1. Correct understanding of developer intent
2. Correctness
3. Preservation of existing behavior
4. Evidence-backed reasoning
5. Architectural fit
6. Minimal safe change
7. Verification
8. Maintainability
9. Speed
MD

3. Add a Stop Review Hook

The Stop Hook reviews whether Claude should end the current assistant turn.

The important distinction is:

ending the current turn

is not the same as:

finishing the entire project

If Claude genuinely needs a user decision, approval, or additional information, returning control to the user is correct.

Run:

python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".claude/settings.json"
if p.exists():
data = json.loads(p.read_text())
else:
data = {}
prompt = r"""You are reviewing whether Claude may end the CURRENT TURN.
IMPORTANT:
Stopping means ending the current assistant turn and returning control to the user.
It does NOT mean the entire project must already be finished.
ALLOW STOPPING with {"ok": true} when:
1. Claude completed the requested work for this turn.
2. Claude produced the requested investigation or plan.
3. Claude is correctly waiting for a necessary user decision or answer.
4. Further progress requires user approval, credentials, domain judgment, or input.
5. Claude reached a reasonable checkpoint requested by the user.
NEVER block stopping merely because the overall project still has future work.
NEVER force Claude to repeatedly say that it is waiting for the user.
Waiting for necessary user input is a valid reason to end the current turn.
Only return {"ok": false, "reason": "..."} when Claude could and should continue
working autonomously in THIS TURN.
When evaluating a potentially premature stop, check:
- Was the user's actual request satisfied?
- Did Claude stop before performing investigation it could still perform?
- Was a root cause claimed without sufficient evidence?
- Was completeness claimed without verifying the relevant scope?
- Was a reproducible bug investigated using evidence or instrumentation?
- Was an arbitrary heuristic introduced when direct detection was possible?
- Was behavior verified after editing?
- Was an expensive exhaustive investigation performed when lightweight inspection was sufficient?
Do not demand unnecessary extra work.
Return JSON only:
{"ok": true}
or
{"ok": false, "reason": "specific work Claude should continue doing now"}
Context:
$ARGUMENTS"""
hooks = data.setdefault("hooks", {})
stop = hooks.setdefault("Stop", [])
if not stop:
stop.append({"hooks": []})
found = False
for group in stop:
for hook in group.get("hooks", []):
if hook.get("type") == "prompt":
hook["prompt"] = prompt
hook["timeout"] = 30
hook.pop("model", None)
found = True
if not found:
stop[0].setdefault("hooks", []).append({
"type": "prompt",
"timeout": 30,
"prompt": prompt
})
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
print("Updated:", p)
PY

4. Validate the Configuration

python3 -m json.tool ~/.claude/settings.json

If there is no JSON error, restart Claude Code.

Then run:

/hooks

Open the Stop event.

A prompt hook loaded from user settings should be visible.


5. Test the Configuration

Example:

First investigate and create a plan.
Do not modify anything yet.
If a decision is required from me, ask before continuing.

Expected behavior:

investigation
→ plan
→ necessary question
→ Stop Hook
→ control returns to the user

If useful autonomous investigation is still possible, the Stop Hook can instead request that Claude continue.


6. Test Progressive Inspection

Example:

This directory contains many large files.
Do not modify anything.
I only need an initial processing plan.
Use lightweight inspection first and perform deeper inspection only when necessary.

A good approach should resemble:

file listing
→ metadata
→ boundaries/samples
→ targeted inspection
→ full validation only if justified

rather than immediately scanning everything.


7. Roll Back

If backups exist:

cp ~/.claude/settings.json.backup \
~/.claude/settings.json
cp ~/.claude/rules/engineering.md.backup \
~/.claude/rules/engineering.md

Why This Helps

The goal is not simply to make Claude think longer.

The goal is to make its engineering process more consistent:

Claude Code
+
Engineering Rules
+
Stop Review
=
More Reliable Agent Behavior

The highest-value principles are:

  • Understand the real requirement before editing
  • Prefer evidence over plausible stories
  • Trace actual integration paths
  • Instrument reproducible problems instead of repeatedly guessing
  • Avoid arbitrary heuristics when direct detection is possible
  • Use the cheapest sufficient evidence
  • Verify behavior, not only compilation

The result is usually a higher reliability floor rather than a different underlying model.

iStoreOS + OpenClash Side Router


1. What Are We Building?

The final network topology looks like this:

                    Internet
                       |
                 Main Router
                192.168.1.1
                       |
             +---------+---------+
             |                   |
       Normal Devices           R61i
     GW=192.168.1.1        192.168.1.2
             |                   |
          Direct             iStoreOS
                                 |
                             OpenClash
                                 |
                              Mihomo
                                 |
                         VLESS / Other Node
                                 |
                              Internet

Devices that need proxy access:

IP: 192.168.1.xxx
Gateway: 192.168.1.2
DNS: 192.168.1.2
Proxy: None

Devices that do not need proxy access simply keep:

Gateway: 192.168.1.1

Both types of devices can continue using the same Wi-Fi network.

That is the main advantage of this single-NIC side-router setup.


2. Hardware and Software

The actual machine used in this setup:

Device: ThinkPad R61i
CPU: Old x86_64 processor
RAM: About 4 GB
Disk: About 233 GB
NIC: Intel Ethernet
NIC count: 1

Operating system:

iStoreOS / OpenWrt x86_64

Actual runtime environment:

iStoreOS 24.10.x
OpenWrt 24.10 series
Kernel 6.6.x
x86_64

One important lesson:

Always trust the version actually running on the machine, not only the filename of the image you originally downloaded.

Check with:

uname -r
cat /etc/openwrt_release

3. Install iStoreOS

Prepare a USB drive and write the x86_64 iStoreOS image to it.

Common tools include:

Rufus
balenaEtcher
dd

Boot the ThinkPad from USB and install iStoreOS to the internal disk.

After installation, boot into iStoreOS.


4. Do Not Panic When /rom Shows 100%

After installation, running:

df -hT

may show something like:

/dev/root squashfs 220M 220M 0 100% /rom

New OpenWrt users often think:

The root filesystem is full.

This is normal.

OpenWrt/iStoreOS uses a read-only SquashFS base system:

/rom → Read-only system image
/overlay → Writable storage for installed packages and changes

The important filesystem to check is:

df -hT /overlay

In this deployment, /overlay was initially only around:

1.9 GB

That may technically work, but it is small if you plan to install:

OpenClash
Mihomo
Ruby
Docker
Additional plugins

So /overlay was expanded to:

16 GB

5. Disk Layout: Do Not Waste the Entire Disk on the OS

There is no reason to use the entire 233 GB disk for /overlay.

The final layout:

/dev/sda
├── sda1 128 MB /boot
├── sda2 256 MB /rom
├── sda3 16 GB /overlay
└── sda4 216 GB /mnt/data

The idea is:

16 GB
→ iStoreOS / OpenClash / Plugins / Docker
Remaining ~216 GB
→ NAS / File Sharing / Downloads / Data

For a home side router, 16 GB of overlay is already very comfortable.


6. Expand /overlay to 16 GB

Before changing partitions, always check:

lsblk
df -hT

Confirm:

Which partition is mounted as /overlay
What the original starting sector is

If you expand an ext4 partition by deleting and recreating the partition table entry:

The new partition must use the exact same starting sector as the original partition.

Otherwise, the filesystem may be destroyed.

After enlarging the partition itself:

resize2fs /dev/sda3

Then verify:

df -hT /overlay

In this case:

/dev/sda3 ext4 15.7G

Expansion succeeded.

Important Warning

After the overlay partition is working:

Do not format sda3 again.

Never run:

mkfs.ext4 /dev/sda3

because /dev/sda3 is the live /overlay filesystem.


7. Create a Data Partition from the Remaining Disk Space

Use the remaining disk capacity for:

/dev/sda4

Format it:

mkfs.ext4 -L data /dev/sda4

Create a mount point:

mkdir -p /mnt/data

Mount it:

mount /dev/sda4 /mnt/data

Verify:

df -hT /mnt/data

In this deployment, about:

212 GB

was available.

Test writing:

touch /mnt/data/test.txt
ls -l /mnt/data/

If you see:

lost+found
test.txt

read/write access is working.


8. Configure Automatic Mounting for the Data Partition

First detect block devices:

block detect

Find the UUID of /dev/sda4.

Example:

UUID = <DATA-PARTITION-UUID>

Add it to iStoreOS fstab through UCI:

uci add fstab mount
uci set fstab.@mount[-1].uuid='<DATA-PARTITION-UUID>'
uci set fstab.@mount[-1].target='/mnt/data'
uci set fstab.@mount[-1].fstype='ext4'
uci set fstab.@mount[-1].enabled='1'
uci commit fstab

Verify:

tail -n 10 /etc/config/fstab

Expected structure:

config mount
option uuid '<DATA-PARTITION-UUID>'
option target '/mnt/data'
option fstype 'ext4'
option enabled '1'

Test the automatic mount mechanism:

umount /mnt/data
block mount
df -hT /mnt/data

If /dev/sda4 appears again at /mnt/data, the configuration is correct.

Finally, perform a real reboot and verify again:

df -hT /mnt/data

9. Set the SSH Root Password

A fresh iStoreOS installation may display:

There is no root password defined on this device!

Immediately set one:

passwd

After that:

ssh root@192.168.1.2

can be used for administration.

Never publish the root password in documentation or screenshots.


10. The Key to a Single-NIC Side Router: Use a Static LAN Address

Assume the existing main router is:

192.168.1.1

We want the side router to use:

192.168.1.2

But do not simply assume .2 is free.

First test:

ping -c 3 192.168.1.2

Then:

ip neigh show 192.168.1.2

If no device responds consistently, .2 is probably available.

A better long-term approach is to choose an address outside the DHCP pool or reserve .2 on the main router.


11. Set the R61i to 192.168.1.2

Back up the network configuration first:

cp /etc/config/network /etc/config/network.bak

Then configure:

uci set network.lan.proto='static'
uci set network.lan.ipaddr='192.168.1.2'
uci set network.lan.netmask='255.255.255.0'
uci set network.lan.gateway='192.168.1.1'
uci -q delete network.lan.dns
uci add_list network.lan.dns='192.168.1.1'
uci commit network

Restart networking:

/etc/init.d/network restart

The SSH session will disconnect. That is expected.

Reconnect:

ssh root@192.168.1.2

Check routing:

ip route

Expected:

default via 192.168.1.1 dev br-lan
192.168.1.0/24 dev br-lan ...

Test the main router:

ping -c 3 192.168.1.1

Then test Internet connectivity:

ping -c 3 openwrt.org

If both work:

LAN ✓
Gateway ✓
DNS ✓
Internet ✓

12. Pitfall: No IP Does Not Automatically Mean DHCP Failure

During deployment, the NIC once entered:

eth0 NO-CARRIER
br-lan NO-CARRIER

At that point, do not immediately assume:

IP conflict
DHCP failure
Wrong gateway

because:

NO-CARRIER

means the Ethernet physical link itself is not established.

Check:

ethtool eth0

Focus on:

Link detected: yes/no

If:

Link detected: no

investigate:

Ethernet cable
Router/switch port
RJ45 connector
PHY
Auto-negotiation
Old NIC hardware
Driver

The R61i used the Intel e1000e driver and at one point reported:

Hardware Error

A full shutdown/power cycle restored the link.

So on old hardware:

If you see NO-CARRIER, check the physical link first. Do not start changing IP settings.


13. Pitfall: Stop Immediately if You See I/O error

This was the most serious issue encountered during deployment.

The kernel reported:

SQUASHFS error
EXT4-fs error
Detected aborted journal
I/O error, dev sda
Buffer I/O error
I/O error while writing superblock
Remounting filesystem read-only

At the same time, strange symptoms appeared:

opkg malfunctioned
dmesg returned I/O errors
LuCI modules appeared to disappear

This is not a normal cache issue.

Correct diagnostic command:

busybox dmesg | grep -iE \
'I/O error|SQUASHFS error|EXT4-fs error|ata.*error|reset|failed'

If you see:

I/O error, dev sda

stop:

Installing packages
Resizing filesystems
Formatting
Heavy disk writes
NAS writes

A full shutdown may be appropriate:

poweroff

In this deployment, a complete power-off restored normal behavior, and the error did not reappear during later testing.

However:

That does not prove the disk is healthy.

If the error ever appears again, investigate:

SMART
HDD condition
SATA link
SATA controller

Do not dismiss it as “cache”.

Also, do not store the only copy of important family photos or documents on an old disk with this history.


14. Pitfall: No root node was registered in LuCI

At one point LuCI displayed:

No root node was registered
Install luci-mod-admin-full and retry.

The obvious reaction might be:

Reinstall LuCI
Install luci-mod-admin-full

But in this case the error occurred while the disk itself was experiencing I/O failures.

So if:

LuCI worked before
Modules suddenly disappear
Commands also behave strangely

do not immediately reinstall packages.

First check:

busybox dmesg | tail -n 100

Make sure the underlying storage is healthy enough to read system files.


15. Prepare to Install OpenClash

OpenClash may not be available in the iStore application store.

In this deployment, OpenClash was installed manually from the official release.

First confirm the package manager:

which opkg
opkg --version

Then:

opkg update

16. Pitfall: Chinese OpenWrt Mirrors May Be Slow from Japan

The default OpenWrt feeds were pointing to:

mirrors.cernet.edu.cn

From Japan, downloads frequently failed with:

curl: (28) Connection timed out

This especially affected:

kmods
ruby

A practical fix is to switch to the official OpenWrt download server.

Back up the feed configuration:

cp /etc/opkg/distfeeds.conf \
/etc/opkg/distfeeds.conf.bak

Replace:

https://mirrors.cernet.edu.cn/openwrt

with:

https://downloads.openwrt.org

Command:

sed -i \
's#https://mirrors.cernet.edu.cn/openwrt#https://downloads.openwrt.org#g' \
/etc/opkg/distfeeds.conf

Then:

opkg update

If you are physically located in China, the original mirror may actually be faster. Use whichever source is reliable from your current network.


17. Verify Kernel and kmods Compatibility

Before installing kernel-dependent packages:

uname -r
opkg list-installed kernel

Make sure the running kernel and kmod feed correspond to each other.

Do not force-install kmods built for another kernel.


18. Install OpenClash Dependencies

Typical OpenClash dependencies include:

bash
dnsmasq-full
curl
ca-bundle
ip-full
ruby
ruby-yaml
kmod-tun
kmod-inet-diag
unzip
kmod-nft-tproxy
luci-compat
luci
luci-base

Check what is already installed:

for p in \
ip-full ruby ruby-yaml kmod-inet-diag unzip \
kmod-nft-tproxy luci-compat luci luci-base
do
opkg list-installed "$p"
done

In this case the main missing packages were:

ruby
ruby-yaml

Install them:

opkg install ruby ruby-yaml

Verify:

ruby -v

19. Install the Official OpenClash IPK

Download the current OpenClash release from the official GitHub repository.

Example:

curl -L --retry 2 \
https://github.com/vernesong/OpenClash/releases/download/<VERSION>/luci-app-openclash_<VERSION>_all.ipk \
-o /tmp/openclash.ipk

Install:

opkg install /tmp/openclash.ipk

Verify:

opkg list-installed | grep openclash

Expected:

luci-app-openclash - <VERSION>

20. OpenClash Installed Does Not Mean Mihomo Core Is Installed

Check:

ls -lh /etc/openclash/core/

If it is empty, you only installed the OpenClash management interface.

The actual proxy engine still needs to be installed.

OpenClash includes a core download script:

/usr/share/openclash/openclash_core.sh

Run:

/usr/share/openclash/openclash_core.sh

Then:

ls -lh /etc/openclash/core/

You should see:

clash_meta

Verify:

/etc/openclash/core/clash_meta -v

Expected output should include something similar to:

Mihomo Meta ...
linux amd64

21. Pitfall: The “Download Core” Button May Download to Your Browser PC

The OpenClash version-update page may contain download icons.

Be careful:

Some of these buttons trigger a browser download on the computer you are using to access LuCI.

That does not mean the file was installed on the R61i.

Always verify through SSH:

ls -lh /etc/openclash/core/

The most reliable method is:

/usr/share/openclash/openclash_core.sh

22. Add a Test Subscription

For real long-term use, use your own:

VLESS Reality
VMess
Trojan
Shadowsocks

For initial architecture testing, a temporary public test subscription can be used.

In OpenClash:

Configuration Subscription
→ Add

Enter:

Configuration Name: test
Subscription URL: https://...

For the first test, keep most advanced options at their defaults.

Save.


23. Saving a Subscription Does Not Always Mean the YAML Was Updated

This is an easy mistake to make.

After editing a subscription URL, clicking:

Save Configuration

may only save the subscription settings.

Check the actual YAML:

ls -lh /etc/openclash/config/test.yaml

If the modification time did not change, the new subscription has not been downloaded yet.

Go back to:

OpenClash → Configuration Subscription

and click:

Update

for that specific subscription.

Then check again:

ls -lh /etc/openclash/config/test.yaml

Only when the file time and/or size changes do you know the actual YAML was refreshed.


24. Inspect What Nodes the Subscription Really Contains

Do not trust only the web interface.

Use SSH:

grep -E \
'^[[:space:]]*-[[:space:]]*name:' \
/etc/openclash/config/test.yaml | head -n 50

You may see:

HK
JP
SG
US
...

This is one of the simplest ways to verify what is actually inside the configuration.


25. Start OpenClash

Check whether OpenClash is enabled:

uci get openclash.config.enable

Expected:

1

Check the selected configuration:

uci get openclash.config.config_path

Example:

/etc/openclash/config/test.yaml

Start:

/etc/init.d/openclash start

Check status:

/etc/init.d/openclash status

Expected:

running

26. Pitfall: start Runs but Service Remains inactive

This actually happened.

Repeatedly running:

/etc/init.d/openclash start
/etc/init.d/openclash status

still returned:

inactive

The real cause was:

uci get openclash.config.enable

returning:

0

Fix:

uci set openclash.config.enable='1'
uci commit openclash
/etc/init.d/openclash start

Lesson:

If OpenClash refuses to start, checking openclash.config.enable should be one of the first troubleshooting steps.

Do not blindly repeat start.


27. Verify That Mihomo Is Actually Listening

Minimal OpenWrt systems may not have:

ss

Use BusyBox instead:

busybox netstat -lntp | \
grep -E '7890|7891|7892|7893|9090'

A healthy running instance may show:

7890 LISTEN
7891 LISTEN
7892 LISTEN
7893 LISTEN
9090 LISTEN

Only then can you confidently say that the proxy core is really running.


28. MetaCubeXD Shows ERR_CONNECTION_REFUSED

The dashboard is usually available at:

http://192.168.1.2:9090/ui/

If the browser reports:

ERR_CONNECTION_REFUSED

do not start troubleshooting the browser first.

Check:

/etc/init.d/openclash status

Then:

busybox netstat -lntp | \
grep -E '7890|7891|7892|7893|9090'

If OpenClash is:

inactive

and none of the ports are listening:

The problem is not MetaCubeXD. OpenClash/Mihomo is simply not running.


29. Select a Node in MetaCubeXD

Open:

Proxies

Find the actual selector group.

Example:

Select Exit

Choose the desired node, for example:

HK Node

Be aware that Clash configurations often contain nested proxy groups.

Example:

Manual Selection
↓
Country Group
↓
Specific Node

Selecting a node inside the country group does not necessarily mean the top-level traffic selector is using that country group.

Always inspect the full chain:

Top-Level Group
↓
Country Group
↓
Specific Node

30. Use curl for a Non-Destructive First Test

Before changing Windows or Android gateway settings, test from a Linux client.

Direct connection:

curl -4 https://api.ipify.org ; echo

Record the public IP.

Then explicitly use the R61i proxy:

curl -4 --max-time 20 \
-x http://<USER>:<PASSWORD>@192.168.1.2:7890 \
https://api.ipify.org ; echo

If the public IP changes, the complete chain works:

Client
↓
R61i
↓
Mihomo
↓
Proxy Node
↓
Internet

The major advantage of this test:

It does not modify the client’s default gateway, DNS, or system proxy settings.

When the command ends, the client is back to normal.


31. What Does HTTP 407 Mean?

If you see:

CONNECT tunnel failed, response 407

that means:

Proxy Authentication Required

In fact, that proves:

Client → R61i:7890

is reachable.

Only authentication is missing.

Check:

uci show openclash | grep authentication

If username/password authentication is enabled, explicit proxy tests must include those credentials.


32. Explicit Proxy Authentication Is Not Required for Transparent Routing

This is another very important distinction.

Explicit proxy:

curl
↓
192.168.1.2:7890
↓
Mihomo

may require username/password.

Transparent side-router mode:

Phone
↓
Gateway 192.168.1.2
↓
R61i firewall redirects traffic
↓
Mihomo

does not require entering the OpenClash proxy username/password on the phone.

Therefore, on Android:

Proxy: None

is correct.


33. Xiaomi / Android Side-Router Configuration

The phone still connects to the original home Wi-Fi.

Open:

Settings
→ Wi-Fi
→ Current Wi-Fi
→ Modify Network
→ IP Settings
→ Static

Suppose DHCP originally gave the phone:

IP: 192.168.1.50
Gateway: 192.168.1.1

Change it to:

IP Address: 192.168.1.50
Gateway: 192.168.1.2
Network Prefix: 24
DNS 1: 192.168.1.2
Proxy: None

Important

Do not blindly copy:

192.168.1.50

That is only an example.

Use the phone’s original IP or an address properly reserved on the main router.

Otherwise, you may create an IP conflict.


34. Windows Works the Same Way

Windows can continue using the same Wi-Fi.

Configure IPv4 manually:

IP: Existing 192.168.1.xxx
Mask: 255.255.255.0
Gateway: 192.168.1.2
DNS: 192.168.1.2

No need to install:

Clash
V2Ray
VPN client
Browser proxy

35. Normal Devices and Proxied Devices Can Coexist

This is one of the biggest practical advantages of this design.

Home Wi-Fi
|
+-- Family Phone
| GW 192.168.1.1
| → Direct
|
+-- Smart TV
| GW 192.168.1.1
| → Direct
|
+-- Personal Phone
| GW 192.168.1.2
| → OpenClash
|
+-- Workstation
GW 192.168.1.2
→ OpenClash

No second Wi-Fi SSID is required.


36. “Global Proxy” Does Not Mean “The Whole House Automatically Uses the Proxy”

OpenClash modes such as:

Rule
Global
Direct

control:

How traffic that has already entered OpenClash is handled.

They do not decide which LAN devices send traffic through the R61i.

A client using:

Gateway = 192.168.1.1

will not suddenly start using the R61i just because OpenClash is in GLOBAL mode.

Only clients using:

Gateway = 192.168.1.2

will pass through the side router.


37. Public Free Nodes Are Good for Testing, Not Long-Term Use

A free HK node successfully proved that:

Japanese Home Public IP
↓
R61i / OpenClash
↓
HK Node
↓
Different Public IP

So the architecture worked.

However, the same public free node also showed:

20-second timeouts
Very slow connections
Node failures
Changing exit locations

Public IP lookup sometimes appeared as:

HK

and sometimes:

US

Possible causes include:

Automatic selection
Changing node pool
Dead nodes
Subscription refresh
Nested proxy groups

Conclusion:

Public free nodes are useful only for proving that the system works.

For long-term use, use your own server or a reliable paid service.


38. Do Not Spend Too Much Time Hunting Free Mainland China Nodes

Public mainland-China exit nodes were also tested.

Observed behavior included:

2–4 second latency
SSL_ERROR_SYSCALL
wrong version number
400 Bad Request
timeouts

Multiple free sources had unstable or unusable CN nodes.

The practical conclusion:

Free, public, stable mainland-China exit IPs are inherently difficult to find.

If your goal is simply to access ordinary Chinese-language websites, HK may already be enough.

If you specifically need:

Mainland-only video licensing
Mainland music licensing
China game servers
Bank/government IP checks

then use a proper mainland-China return-acceleration service.


39. Recommended Final Node: Your Own VLESS Reality Server

If you already operate your own:

VLESS Reality

server, that is the preferred final solution.

A client QR code usually contains something equivalent to:

vless://...

with parameters such as:

Server
Port
UUID
Reality Public Key
Short ID
SNI
Fingerprint
Flow
Transport

Do not publish the full URI.

The correct process is:

Client QR Code
↓
Extract vless:// URI
↓
Convert to Mihomo/Clash configuration
↓
OpenClash
↓
Test

40. How to Make the Router Nearly Plug-and-Play After Moving to China

The easiest approach is to make the LAN layout identical in Japan and China.

Recommended:

Main Router: 192.168.1.1
R61i: 192.168.1.2

Before moving, make sure:

192.168.1.2

is not used by another device on the China network.

After moving:

China ISP
|
Main Router
192.168.1.1
|
LAN
|
R61i
192.168.1.2

The R61i can keep the same:

iStoreOS
OpenClash
Mihomo
VLESS

configuration tested in Japan.

Clients that need proxy access simply use:

Gateway = 192.168.1.2
DNS = 192.168.1.2

41. What If the China Router Is Not 192.168.1.1?

For example, if the router uses:

192.168.31.1

then the R61i should not remain:

192.168.1.2

Instead use:

Main Router: 192.168.31.1
R61i: 192.168.31.2

Clients should then use:

Gateway = 192.168.31.2

So before moving, check the LAN subnet of the destination router.


42. Reboot Recovery Is Part of Final Acceptance Testing

Once everything works, test:

reboot

For final validation, also test a complete shutdown:

poweroff

Disconnect power, power it back on, and verify:

uci get openclash.config.enable
uci get openclash.config.config_path
/etc/init.d/openclash status

Expected:

1
/etc/openclash/config/xxx.yaml
running

Then:

busybox netstat -lntp | \
grep -E '7890|7891|7892|7893|9090'

If everything returns automatically, the deployment is truly complete.


43. Troubleshooting Quick Reference

SymptomFirst Thing to Check
R61i has no IPip a
NIC shows NO-CARRIERethtool eth0
SSH works but LuCI behaves strangelybusybox dmesg
opkg suddenly gives I/O errorsCheck HDD/SATA
I/O error, dev sdaStop disk writes and investigate storage
OpenClash shows inactiveuci get openclash.config.enable
Port 9090 refuses connectionOpenClash status + netstat
Port 7890 returns HTTP 407Proxy authentication
Node sometimes times outTest another node
Configuration switched but nodes did not changeRestart OpenClash
Subscription URL changed but YAML did notClick subscription “Update”
MetaCubeXD uses the wrong exitCheck nested proxy groups
/rom shows 100%Normal; check /overlay

44. Useful Daily Diagnostic Commands

Network:

ip a
ip route
ping -c 3 192.168.1.1

Disk:

lsblk
df -hT
mount | grep -E 'overlay|/mnt/data'

Disk errors:

busybox dmesg | grep -iE \
'I/O error|SQUASHFS error|EXT4-fs error|ata.*error'

OpenClash:

uci get openclash.config.enable
uci get openclash.config.config_path
/etc/init.d/openclash status

Mihomo:

busybox netstat -lntp | \
grep -E '7890|7891|7892|7893|9090'

Public IP:

curl -4 https://api.ipify.org ; echo

45. Optional Next Step: Turn /mnt/data into a Home NAS

The disk is already prepared:

/dev/sda4
↓
/mnt/data
↓
About 212 GB

A useful directory layout could be:

/mnt/data
├── public
├── transfer
├── private
└── backup

Then install either:

Samba4

or:

ksmbd

to expose SMB shares to the LAN.

Typical access paths:

Windows:
\\192.168.1.2\public
Android:
smb://192.168.1.2/public
Linux:
smb://192.168.1.2/public

LAN file transfers stay inside the local network and do not use the Internet connection.

This turns the old ThinkPad into:

Side Router
+
Transparent Proxy Gateway
+
Home NAS

46. Final Result

After all of the above, the old ThinkPad becomes:

ThinkPad R61i
|
+-- iStoreOS
|
+-- 192.168.1.2 Side Router
|
+-- OpenClash
|
+-- Mihomo
|
+-- VLESS Reality
|
+-- 16 GB Overlay
|
+-- ~212 GB Data Partition
|
+-- Optional Samba NAS

For an old single-NIC laptop, this setup has several strong advantages:

  1. The existing main router does not need to be replaced.
  2. The existing Wi-Fi network remains unchanged.
  3. Only selected devices need to use the proxy.
  4. Client devices do not need proxy software.
  5. OpenClash nodes can be changed at any time.
  6. If the LAN subnet is kept the same, moving the machine from Japan to China requires very little reconfiguration.
  7. The remaining hard disk space can be reused as a home NAS.

The most important deployment lesson is:

Validate one layer at a time: disk → LAN → gateway/DNS → OpenClash → Mihomo → proxy node → explicit proxy → transparent routing → reboot recovery. Do not change several layers at once.

Following this order makes troubleshooting dramatically easier because you can quickly identify whether a problem is caused by:

Hardware
Filesystem
Network
OpenClash
Mihomo
Proxy node
Client configuration

47. What “Finished” Really Means

The project is not complete just because the OpenClash web page loads.

It is also not complete just because a node appears green.

A truly finished system should pass this full chain:

R61i cold boot
↓
iStoreOS starts automatically
↓
/mnt/data mounts automatically
↓
OpenClash starts automatically
↓
Mihomo listens normally
↓
Your own VLESS Reality node reconnects
↓
Phone only uses Gateway=192.168.1.2
↓
No proxy app installed on the phone
↓
Target Internet access works reliably

At that point, the old ThinkPad has genuinely become a practical home side-router server rather than just an experimental OpenWrt machine.

Converting a Lenovo Flex 10 into a Batocera FC/NES


Environment

Hardware:

Lenovo Flex 10
Intel Atom Z36xx/Z37xx
Intel integrated graphics
500GB Seagate HDD
HDMI

System:

Batocera x86_64

Batocera was initially running correctly from a ~32GB USB drive.

Disk layout:

/dev/sda 465.8G internal HDD
/dev/sdb 29.3G Batocera USB

The Batocera USB contained:

sdb1 10G /boot
sdb2 19.2G /userdata

1. Migrating Batocera from USB to the Internal HDD

The first option considered was Batocera’s built-in installer:

batocera-install listDisks
batocera-install listArchs
batocera-install listFiles

listDisks correctly detected the internal HDD:

sda ST500LT012-1DG142 (465.8G)

However, in this environment, listArchs and listFiles did not provide a usable installation source.

Since the USB already contained a fully working and bootable Batocera installation, I decided to perform a block-level clone instead:

dd if=/dev/sdb of=/dev/sda bs=16M

Pitfall: BusyBox dd Compatibility

A more typical command would be:

dd if=/dev/sdb of=/dev/sda bs=16M status=progress conv=fsync

However, the dd implementation included in this Batocera environment was a reduced BusyBox version, and some GNU dd options were unsupported.

The command therefore failed with an unsupported operand error.

The simple version worked:

dd if=/dev/sdb of=/dev/sda bs=16M

After cloning, I removed the USB drive and booted directly from the internal HDD.

Batocera booted successfully.


2. Why a 500GB HDD Only Showed About 30GB After Cloning

This is expected when cloning the entire source disk with dd.

The original USB layout was approximately:

/dev/sdb1 10G
/dev/sdb2 19.2G

After cloning it onto the 465.8GB HDD, the target inherited the same partition table:

/dev/sda1 10G
/dev/sda2 19.2G

The remaining ~436GB was left unallocated.

Instead of creating another partition, I decided to expand:

/dev/sda2 -> /userdata

This keeps the Batocera storage layout simple, since ROMs, BIOS files, saves, screenshots, and configuration all live under /userdata.


3. Expanding /userdata with Ubuntu Live and GParted

I booted an Ubuntu Live USB and opened GParted.

At that point, GParted reported a GPT warning similar to:

The backup GPT table is not at the end of the disk

This was expected.

The disk had been cloned from:

32GB source disk
↓
500GB target disk

The backup GPT header was still located where the end of the original 32GB device had been.

I selected:

Fix

GParted then relocated the backup GPT metadata to the actual end of the 500GB HDD.

Next, I resized /dev/sda2.

The original state was approximately:

Free space preceding: 0 MiB
New size: 19711 MiB
Free space following: 446988 MiB

I changed it to:

Free space preceding: 0
Free space following: 0

In other words, I left the start of the partition untouched and extended only its end.

The final layout became:

sda1 ~10G /boot
sda2 remaining /userdata

This is cleaner than creating an additional partition just for ROM storage.


4. HDMI Black Screen: Do Not Assume the Driver Is Missing

After the HDD migration was complete, the next problem was HDMI output.

The TV detected an HDMI connection, but no image appeared.

The first step was to verify the Intel graphics stack:

lspci -nnk | grep -A4 -Ei 'VGA|Display'
lsmod | grep i915

The i915 driver was loaded correctly.

Next:

ls /sys/class/drm/

The system exposed:

card0-eDP-1
card0-HDMI-A-1
card0-DP-1
card0-VGA-1

The internal panel was eDP-1, while HDMI was exposed through HDMI-A-1.

Checking the connector status:

cat /sys/class/drm/card0-HDMI-A-1/status

returned:

connected

At this point, several possible causes could already be eliminated:

  • Intel graphics driver not loaded
  • HDMI connector not detected
  • HDMI hotplug failure
  • Basic hardware detection failure

The problem was clearly higher up in the display configuration stack.


5. Batocera Also Detected the HDMI Output

Batocera’s own display utility also recognized the HDMI connection:

batocera-resolution listOutputs

The result included:

HDMI-1

Available modes could also be queried:

batocera-resolution listModes

Valid HDMI modes were reported.

The effective path was therefore:

Intel iGPU
↓
i915
↓
DRM connector
↓
HDMI connected
↓
EDID / display modes detected
↓
Batocera sees HDMI-1

Yet there was still no usable image on the TV.

At this stage, the issue was clearly a display configuration problem, not a driver problem.


6. xrandr Was the Actual Fix

I checked the X display outputs:

xrandr

On this machine:

eDP-1 internal panel
HDMI-1 TV

I then explicitly enabled HDMI:

xrandr --output HDMI-1 --mode 1280x720 --rate 60

The TV immediately displayed an image.

That confirmed that:

The HDMI hardware, EDID, kernel driver, and X output were all functioning correctly. The default display layout simply was not enabling the HDMI output as intended.


7. Disabling the Internal Panel and Using HDMI Only

Enabling HDMI alone resulted in an extended desktop.

That was not the desired behavior for a dedicated console.

The target configuration was:

Internal display: OFF
HDMI: ON
HDMI: primary

The working command was:

xrandr \
--output eDP-1 --off \
--output HDMI-1 --mode 1280x720 --rate 60 --primary

After running it:

eDP-1 off
HDMI-1 primary
1280x720 @ 60Hz

This gave the expected console-style behavior.


8. Why I Chose 720p Instead of 1080p

The Flex 10 uses an older Atom platform, and the intended systems are primarily:

NES / FC
SNES
Mega Drive / Genesis
lightweight arcade emulation

There is little practical benefit in forcing the front end to run at 1080p just because the TV supports it.

I therefore fixed the output at:

1280x720 @ 60Hz

For this hardware and workload, it is a sensible configuration.


9. HDMI Auto-Switch Script

I created a simple switching script with the following logic:

HDMI connected
↓ yes
HDMI primary
eDP off
HDMI disconnected
↓
eDP primary
HDMI off

Script:

#!/bin/sh
case "$1" in
start)
sleep 5
if [ "$(cat /sys/class/drm/card0-HDMI-A-1/status)" = "connected" ]; then
xrandr \
--output HDMI-1 --mode 1280x720 --rate 60 --primary \
--output eDP-1 --off
else
xrandr \
--output eDP-1 --auto --primary \
--output HDMI-1 --off
fi
;;
esac

Manual execution worked correctly:

/userdata/system/custom.sh start

10. The Batocera v43 custom.sh Pitfall

This was one of the more useful version-specific findings.

Searching the init scripts:

grep -R "custom.sh" /etc/init.d /etc 2>/dev/null | head -30

revealed:

custom.sh : deprecated in v43

Older Batocera guides frequently rely on:

/userdata/system/custom.sh

as an automatic startup hook.

In Batocera v43, that mechanism has changed, and user services under:

/userdata/system/services/

are now used instead.

The migrated service could be enabled with:

batocera-services enable custom_service

Manual execution also worked:

/userdata/system/services/custom_service start

However, even after enabling the service, the HDMI layout still did not switch correctly after reboot.

Since the same script worked perfectly when started manually, the likely issue was not the script itself.

The more likely cause was startup timing and the X environment, for example:

  • service starts before X is ready
  • DISPLAY is not yet available
  • EmulationStation has not fully initialized its display state
  • another display initialization step later overrides the configuration

This could be solved with further work around service timing or EmulationStation startup hooks, but for this machine the benefit was not worth the additional effort.

The practical workaround is simply:

/userdata/system/services/custom_service start

when HDMI output needs to be switched manually.


11. NES ROM Directory

FC/NES ROMs go in:

/userdata/roms/nes/

For example:

/userdata/roms/nes/Adventure Island.nes
/userdata/roms/nes/Battle City.nes
/userdata/roms/nes/Double Dragon II.nes

After copying ROMs, refresh the EmulationStation game list.

If necessary, reboot Batocera so the front end rescans the directory.


12. Final System Layout

The resulting setup looks like this:

Lenovo Flex 10
│
├── Batocera x86_64
│
├── 500GB HDD
│ ├── /boot
│ └── /userdata
│ └── roms/nes
│
├── Intel i915
│
└── HDMI
└── 1280x720 @ 60Hz
└── TV as primary display

Key Takeaways

The four issues that were actually worth documenting were:

1. BusyBox dd option compatibility inside Batocera
2. GPT repair and partition expansion after cloning a small disk to a larger disk
3. HDMI "connected but no picture" does not necessarily mean a missing graphics driver
4. Batocera v43 changed the old custom.sh startup mechanism

The HDMI troubleshooting sequence was especially useful:

Is i915 loaded?
↓
Does the DRM HDMI connector exist?
↓
Is status = connected?
↓
Does Batocera detect the HDMI output?
↓
Does xrandr see the output and valid modes?
↓
Force the desired output configuration

If i915 is loaded, the DRM connector exists, HDMI status is connected, and valid EDID modes are available, there is little reason to waste time searching for another HDMI driver.

In this case, the final working command was simply:

xrandr \
--output eDP-1 --off \
--output HDMI-1 --mode 1280x720 --rate 60 --primary

The Case of the Disappearing Drive: How Windows Corrupted a Linux exFAT “Superfloppy”


### Introduction

 Have you ever plugged an external USB hard drive into a Linux machine, only to find that it appears completely empty—no
 partitions, no mount points, and no files?

   $ lsblk
   NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
   sdX           8:0    0 931.5G  0 disk  
   nvme0n1     259:0    0   1.8T  0 disk  
   …

 To make matters worse, attempting to manually mount the disk gives you this notorious, generic Linux error:
   $ sudo mount -t exfat /dev/sdX /mnt
   mount: /mnt: wrong fs type, bad option, bad superblock on /dev/sdX, missing codepage or helper program, or other error.
 If this disk worked fine recently, had healthy SMART attributes (0 bad sectors), and was recently plugged into a Windows
 PC, you might have fallen victim to a subtle but notorious storage conflict: Windows corrupting a “Superfloppy” exFAT
 volume.

 In this article, we’ll analyze why this happens, how to safely diagnose it, and how to recover all your data in under a
 minute without losing a single byte.
 ──────
 ### The Anatomy of the Bug: What Actually Happened?

 To understand the cause, we need to examine how the drive was originally created and how different operating systems
 treat external drives.

 #### 1. The “Superfloppy” (Whole-Disk) Formatting Scheme
 Typically, a storage drive is split into:

 • Sector 0: The partition table (MBR or GPT).
 • Sector 2048+: The actual partition (e.g., /dev/sdX1) containing the filesystem.

 However, some external storage drives, cameras, and media devices format the drive as a Superfloppy (whole-disk
 filesystem). In this setup, there is no partition table. The exFAT filesystem begins directly at Sector 0 (/dev/sdX).
 In exFAT, the first 12 sectors are reserved for the Main Boot Region:

 • Sectors 0–10: Volume Boot Record (VBR) and filesystem parameters.
 • Sector 11: 32-bit Boot Checksum verifying sectors 0–10.
 • Sectors 12–23: An exact mirror known as the Backup Boot Region.
 #### 2. The Windows “Drive-By” Modification

 When you plug a 1TB Superfloppy drive into Windows, Windows expects large drives to have standard partition tables
 (MBR/GPT). Because it doesn’t immediately find an sdX1 partition, Windows may consider the disk uninitialized or damaged.

 In the background, Windows Virtual Disk Service (VDS) often attempts to “help” by writing a standard MBR partition
 signature (0x55AA) and an MBR disk identifier into Sector 0.

 Windows assumed it was simply writing an empty MBR table. In reality, it trampled right over the exFAT Volume Boot Record
 (Sector 0)!

 #### 3. The Result in Linux

 When the drive is plugged back into Linux:
 1. lsblk reads Sector 0, sees an MBR table with 0 partition entries, and reports the disk as an unpartitioned blank drive.
 2. The Linux kernel’s native exfat driver reads Sectors 0–10, computes the checksum, and finds that it doesn’t match the
 checksum stored in Sector 11 (because Windows altered several bytes in Sector 0).
 3. The kernel panics safely and logs this error in dmesg:
   exFAT-fs (sdX): Invalid boot checksum (boot checksum : 0x5496345c, checksum : 0xf096346a)
   exFAT-fs (sdX): invalid boot region
   exFAT-fs (sdX): failed to recognize exfat type

 ──────
 ### Diagnostic Steps

 If you suspect you are dealing with this issue, verify it using read-only diagnostic tools.

 #### Step 1: Probe Low-Level Signatures with wipefs
 Run wipefs with no destructive flags. It safely prints all magic bytes and signatures found across the drive:

   sudo wipefs /dev/sdX

 Typical Output:

   DEVICE OFFSET       TYPE  UUID      LABEL
   sdX    0x3          exfat E7D3-FCC1 portableHDD
   sdX    0x1fe        dos              
   sdX    0xe8e0db5e00 gpt

 • Notice exfat at offset 0x3: The exFAT signature starts at byte 3 of the whole disk (/dev/sdX).
 • Notice dos at offset 0x1fe (byte 510): The MBR boot signature written by Windows.

 These conflicting signatures prove the drive has an exFAT filesystem whose boot sector was overlaid with an MBR table.

 #### Step 2: Check Kernel Logs
 Check dmesg after an attempted mount:

   dmesg | tail -n 20

 Look for lines indicating Invalid boot checksum and invalid boot region.
 ──────
 ### The Fix: Recover in One Single Command

 Thanks to the official exFAT specification, Sectors 12–23 contain an exact, untouched mirror of the original boot region
 (Backup Boot Region). Windows only modified Sector 0, leaving the backup untouched.

 Linux provides the official fsck.exfat utility (from exfatprogs) which automatically recognizes this exact failure mode.

 #### Step 1: Dry-Run Verification (Safe, Read-Only)

 Run a non-destructive check:

   sudo fsck.exfat -n -v /dev/sdX

 You should see:

   checksum of boot region is not correct. 0x5496345c, but expected 0xf096346a
   boot region is corrupted. try to restore the region from backup. Fix (y/N)? n

 #### Step 2: Execute the Automatic Restore

 Tell fsck.exfat to restore the corrupted boot sector from the valid backup sector:

   sudo fsck.exfat -y /dev/sdX

 Output:

   checksum of boot region is not correct. 0x5496345c, but expected 0xf096346a
   boot region is corrupted. try to restore the region from backup. Fix (y/N)? y
   /dev/sdX: clean. directories 857, files 7727

 In less than a second, the pristine backup boot sector is written back to Sector 0, the checksum validates, and the
 filesystem structure is restored.

 #### Step 3: Mount Your Restored Drive

 Create a mount directory and mount your drive:

   sudo mkdir -p /media/user/portableHDD
   sudo mount -t exfat -o uid=$(id -u),gid=$(id -g) /dev/sdX /media/user/portableHDD

 Check your files:

   ls -la /media/user/portableHDD

 All your directories and files should now be fully accessible with complete read/write permissions.
 ──────
 ### How to Prevent This in the Future

 While the fsck.exfat -y command is an easy one-liner fix, the best long-term practice is to avoid raw Superfloppy
 formatting on large external hard drives.

 When you have time to back up your files, reformat the drive using a standard GPT partition scheme:

   # 1. Create a standard GPT partition table and partition 1
   sudo parted -s /dev/sdX mklabel gpt mkpart primary exfat 2048s 100%

   # 2. Format the partition (NOT the raw disk) as exFAT
   sudo mkfs.exfat /dev/sdX1 -L “portableHDD”

 By placing the filesystem inside /dev/sdX1 starting at Sector 2048 (1MB offset), the filesystem metadata and the disk
 partition table occupy completely distinct physical sectors. Windows will never accidentally overwrite your boot record again.

Your SSH Port Change Was Ignored: systemd Socket Activation on Ubuntu

TL;DR — On Ubuntu 22.10 and later, sshd is started by systemd socket activation. The listening port belongs to ssh.socket, not to /etc/ssh/sshd_config. Editing Port in sshd_config is silently ignored. Fix it with a ssh.socket drop-in.

The Symptom

You uncomment Port yourNumber in /etc/ssh/sshd_config, restart the service, and nothing changes. What makes this especially confusing is that sshd itself insists the config was read correctly:

$ sudo sshd -T | grep -i "^port"
port yourNumber

$ sudo ss -tlnp | grep sshd
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=174486,fd=3),("systemd",pid=1,fd=202))
LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=174486,fd=4),("systemd",pid=1,fd=203))

The config says yourNumber. The kernel says 22. The giveaway is in that last column: the listening file descriptor is held by systemd (pid=1), not by sshd.

Root Cause

Ubuntu even documents this in the shipped config file, though it is easy to scroll past:

# Port and ListenAddress options are not used when sshd is socket-activated,
# which is now the default in Ubuntu.

Diagnosis in Three Commands

systemctl is-enabled ssh.socket
sudo ss -tlnp | grep sshd
sudo sshd -T | grep -i "^port"
ObservationDiagnosis
enabled + fd owned by systemdSocket activation. The port lives in ssh.socket.
sshd -T shows the new port, ss shows the old oneSame thing — config parsed, then ignored.
sshd -T still shows port 22Your edit was never read. Check sshd_config.d/.
Unit ssh.socket does not existClassic mode. sshd_config is authoritative.

The Fix: A ssh.socket Drop-In

Keep socket activation and put the port where it actually belongs. Use systemctl edit rather than writing under /lib/systemd/system/, so your change survives package upgrades.

sudo systemctl edit ssh.socket

Add:

[Socket]
ListenStream=
ListenStream=0.0.0.0:yourNumber
ListenStream=[::]:yourNumber
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -tlnp | grep ":yourNumber"

The empty ListenStream= line is mandatory. systemd list directives are additive: without the empty assignment to reset the list, you append yourNumber to the inherited 22 and keep listening on both. Omitting it produces no warning and no error — just a config that looks right and does nothing you wanted.

Alternative: Turn Socket Activation Off

If you would rather have sshd_config be the single source of truth — which you need anyway for ListenAddress or Match blocks — revert to the traditional daemon:

sudo systemctl disable --now ssh.socket
sudo systemctl enable ssh.service
sudo systemctl daemon-reload
sudo systemctl restart ssh.service

If port 22 is still held afterwards, a leftover drop-in is re-enabling socket behaviour. Move it aside and reload:

ls -l /etc/systemd/system/ssh.service.d/
sudo mv /etc/systemd/system/ssh.service.d/00-socket.conf /root/
sudo systemctl daemon-reload && sudo systemctl restart ssh.service

Which Approach?

Drop-in on ssh.socketDisable socket activation
Where the port is configuredsystemd unitsshd_config
ListenAddress / Match honouredNoYes
Memory footprintOn demandResident (a few MB)
Survives apt upgradeYesYes
Chance of confusing you again in six monthsHigherLower

Don’t Lock Yourself Out

sudo ufw allow yourNumber/tcp
# from a SECOND terminal, with the original session still open:
ssh -p yourNumber user@host
# only once that succeeds:
sudo ufw delete allow 22/tcp
ItemNote
Existing sessionKeep the current SSH window open until the new port is verified.
Out-of-band accessConfirm you can reach a serial/VNC console before restarting.
Cloud firewallSecurity groups on AWS/Azure/GCP are a separate allow-list from ufw.
Stale port 22If it lingers after disable --now, a reboot releases it for good.
Never edit/lib/systemd/system/ssh.socket — overwritten on package update.
Rollbacksudo systemctl revert ssh.socket && sudo systemctl restart ssh.socket

Takeaway

sshd -T tells you what sshd believes. ss -tlnp tells you what the kernel is doing. When the two disagree, look at who owns the file descriptor — that column is where the real answer is. Under socket activation, systemd owns the socket and sshd is merely handed the result.

Transcribe Audio to Text with Microsoft 365 Word Online

This method works not only for English, but also for Japanese and other languages. Here are the steps for using the Transcribe feature in Word for the web (Word Online).

Steps

  1. Sign in to the official Microsoft 365 site and open Word for the web (Word Online).
  2. Create a new blank document.
  3. Click the Home tab in the top menu.
  4. On the right side of the Home tab, click the down arrow next to the Dictate button and select Transcribe.
    • If Transcribe does not appear, the feature may not be enabled for your account, or it may be limited by your version.
  5. A transcription sidebar opens on the right. Click Upload audio and select the .m4a file you want to transcribe.
  6. Once the upload finishes, transcription starts automatically. The resulting text appears in the sidebar when it is done.
  7. You can then click part or all of the transcript to insert it into the document, or simply copy and paste it.

Key points

  • Supports multiple languages, including English and Japanese, with good accuracy.
  • Handy for turning personal recordings or meeting audio into text.
  • The uploaded audio and the transcript are temporarily stored in the cloud, so be careful with confidential material.

Build an AI-Native 3D Modeling Environment on Ubuntu 24 with Claude Code + Blender MCP


Why This Workflow?

Traditional Blender tutorials usually teach you to:

  • Learn keyboard shortcuts
  • Learn polygon modeling
  • Learn UV mapping
  • Learn materials
  • Learn rendering
  • Learn animation

It often takes weeks or even months before creating a usable game asset.

With Claude Code and Model Context Protocol (MCP), the workflow changes dramatically.

Instead of manually modeling everything, you simply describe what you want:

Natural Language
↓
Claude Code
↓
Blender MCP
↓
Blender creates the model

Your role shifts from being a modeler to becoming a designer and reviewer.


Final Workflow

After completing this guide, your pipeline will look like this:

Claude Code
│
▼
Blender MCP
│
▼
Blender
│
├── game.glb → Godot
│
└── model.stl → 3D Printing

The same asset can later be used both inside a game and as a printable model.


Development Environment

This article uses the following environment:

SoftwareVersion
Ubuntu24.04
Blender5.x
Claude CodeLatest
uvLatest
Blender MCPLatest

Installing Blender

Instead of using the Ubuntu package repository, download Blender directly from the official website https://www.blender.org

Extract it into a fixed location.

sudo mkdir -p /opt/blender
sudo tar -xJf blender-5.x-linux-x64.tar.xz \
-C /opt/blender \
--strip-components=1

Run Blender:

/opt/blender/blender

If Blender launches successfully, the installation is complete.


Create a Desktop Launcher (KDE)

Create:

/usr/share/applications/blender.desktop

Example:

[Desktop Entry]
Version=1.0
Type=Application
Name=Blender
Exec=/opt/blender/blender %F
Icon=/opt/blender/blender.svg
Terminal=false
Categories=Graphics;

After saving the file, Blender will appear in the KDE launcher and can be pinned to the desktop or taskbar.


Install Claude Code

Verify Claude Code is installed:

claude --version

Example:

2.x.x

Install uv

Verify:

uv --version

Example:

uv 0.11.x

Download Blender MCP

Create a workspace:

mkdir -p ~/dev/mcp
cd ~/dev/mcp
git clone https://github.com/ahujasid/blender-mcp.git

Install the Blender Add-on

Open Blender.

Navigate to:

Edit
↓
Preferences
↓
Add-ons
↓
Install from Disk

Select:

addon.py

Enable the add-on.

Press N inside the 3D Viewport.

You should now see:

BlenderMCP

This confirms that the Blender side has been configured correctly.


Configure Claude Code

Navigate to your project directory.

cd ~/Projects/MyGame

Register the MCP server:

claude mcp add blender uvx blender-mcp

Verify:

claude mcp list

Expected output:

blender
✓ connected

This means Claude Code can now communicate with Blender.


Verify the Connection

First, ask Claude what Blender tools are available:

What Blender MCP tools are available?

Claude should list tools such as:

  • Scene Information
  • Materials
  • Viewport Screenshot
  • PolyHaven
  • Sketchfab
  • Hyper3D Rodin
  • Hunyuan3D

Then ask Claude to inspect the current scene:

Read the current Blender scene.

Typical response:

Camera
Cube
Light

At this point, Claude is communicating with Blender successfully.


Generate Your First AI Model

Now simply ask Claude:

Create a low-poly wooden crate.
Requirements:
- Game Ready
- Low Poly
- Wood Material
- Centered at World Origin
- Suitable for Godot

Claude will:

  1. Control Blender through MCP
  2. Create the mesh
  3. Create materials
  4. Update the Blender scene automatically

No manual modeling is required.


Create an AGENTS.md

One of the biggest productivity improvements is creating an AGENTS.md file inside your project.

Instead of repeating requirements every time, define them once.

Example:

  • Godot 4 project
  • Export GLB
  • Low Poly
  • Triangle count under 500
  • Bottom-center pivot
  • Apply all transforms
  • Save .blend
  • Export .glb

Once configured, a prompt like:

Create a pine tree.

is enough for Claude to generate assets following your project standards automatically.


Result

After completing the setup, your workflow becomes:

Describe what you want
↓
Claude Code
↓
Blender MCP
↓
Blender automatically creates the model

Instead of spending months learning traditional modeling, you can focus on:

  • Game design
  • Gameplay
  • Asset planning
  • Iteration

while AI handles most of the repetitive work.


What’s Next?

The next logical step is integrating Godot into the workflow:

Claude Code
↓
Blender MCP
↓
Godot
↓
AI-generated GDScript
↓
Playable Game Prototype

With that setup, you’ll have a complete AI-assisted game development pipeline running entirely on Ubuntu.

Building Unreal Engine 5.8 from Source on Ubuntu 24.04

A step-by-step guide to building Unreal Engine 5.8 from source on Ubuntu 24.04. This article focuses on environment setup only and does not cover runtime issues such as Vulkan rendering, GPU memory, or project-specific troubleshooting.


Introduction

Unlike Windows, Unreal Engine on Linux is typically built directly from source because Epic Games Launcher is not available.

This article documents the complete environment setup process, including:

  • Obtaining Unreal Engine source access
  • Downloading the source code
  • Installing dependencies
  • Building the engine
  • Configuring the NVIDIA driver
  • Launching Unreal Editor

This guide intentionally excludes runtime troubleshooting, which will be covered separately.


Prerequisites

Before starting, make sure you have:

  • Ubuntu 24.04 LTS
  • Git
  • A GitHub account
  • An Epic Games account

You must link your GitHub account to your Epic Games account.

After accepting Epic’s GitHub organization invitation, you’ll gain access to the Unreal Engine repository.


1. Clone Unreal Engine

Create a workspace:

mkdir -p ~/projects_ue5
cd ~/projects_ue5

Clone the engine:

git clone https://github.com/EpicGames/UnrealEngine.git

The initial clone requires tens of gigabytes of disk space, so ensure sufficient free storage before starting.


2. Download Dependencies

Enter the source directory:

cd UnrealEngine

Run:

./Setup.sh

This script automatically downloads:

  • Third-party libraries
  • Toolchains
  • ShaderCompileWorker dependencies
  • Platform SDKs

During execution, you may notice references to distributions such as Rocky Linux.

This is completely normal.

The script downloads dependencies for multiple supported Linux environments, even when running on Ubuntu.


3. Generate Project Files

Run:

./GenerateProjectFiles.sh

During the first execution, Unreal Engine automatically downloads and configures the required .NET SDK.

If the output reports that the Android SDK is missing, it can safely be ignored unless Android development is required.


4. Build Unreal Engine

The simplest build command is:

make -j$(nproc)

Alternatively, Unreal provides its own build script:

Engine/Build/BatchFiles/Linux/Build.sh UnrealEditor Linux Development

The initial build can take anywhere from tens of minutes to several hours depending on your hardware.

After a successful build, the editor executable is located at:

Engine/Binaries/Linux/UnrealEditor

5. Launch Unreal Editor

Launch the editor:

Engine/Binaries/Linux/UnrealEditor

Or open a specific project:

Engine/Binaries/Linux/UnrealEditor MyProject.uproject

The first launch usually takes significantly longer than subsequent launches because Unreal Engine needs to perform several initialization tasks, including:

  • Starting ShaderCompileWorker
  • Compiling shaders
  • Initializing the Derived Data Cache (DDC)

This behavior is expected.


6. NVIDIA Driver Considerations

When launching Unreal Engine, you may encounter a warning similar to:

Installed Driver: xxx.xx
Recommended Driver: xxx.xx

Avoid upgrading the driver immediately.

Instead, check the Unreal Editor log first.

If the log explicitly reports missing Vulkan extensions, then upgrading the NVIDIA driver is appropriate.

After upgrading, verify the installation using:

nvidia-smi

It is also recommended to verify the Vulkan runtime:

vulkaninfo

Ensure that Vulkan initializes correctly before attempting to launch Unreal Engine again.


7. Verify the Environment

After completing the previous steps, create a new Unreal Engine project.

For the initial verification, it is recommended to use:

  • Games
  • Blank
  • Blueprint

This provides the simplest project configuration for confirming that the development environment has been built successfully.

Once the editor opens correctly, the environment setup is complete.


Useful Commands

Check the NVIDIA driver

nvidia-smi

Check the Vulkan runtime

vulkaninfo

Find large directories

sudo du -xh --max-depth=1 /

Conclusion

Building Unreal Engine 5.8 on Ubuntu mainly consists of the following steps:

  1. Obtain Epic Games source access
  2. Clone the Unreal Engine repository
  3. Run Setup.sh
  4. Run GenerateProjectFiles.sh
  5. Build the engine
  6. Verify or update the NVIDIA driver if necessary
  7. Launch Unreal Editor and create a test project

Although Linux requires source compilation, the overall workflow is straightforward once the environment has been configured correctly.

This article focuses solely on setting up the development environment. Runtime topics such as Vulkan configuration, rendering features, GPU memory management, and Linux-specific troubleshooting will be covered in separate articles.

Ubuntu SSH Port Configuration: Switch Between ssh.service and ssh.socket

Option 1: Restore Traditional ssh.service Mode

This approach is the easiest to understand and is suitable if you previously managed the SSH port through /etc/ssh/sshd_config.

Disable Socket Activation and Enable SSH Service

sudo systemctl disable --now ssh.socket
sudo systemctl enable --now ssh.service

Check Current SSH Port Configuration

grep -n '^Port' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/* 2>/dev/null

If no custom port is configured, add the following line to /etc/ssh/sshd_config:

Port <CUSTOM_PORT>

Restart SSH Service

sudo systemctl restart ssh.service

Verify Listening Port

sudo ss -ltnp | grep <CUSTOM_PORT>

If you see output such as:

*: <CUSTOM_PORT>

or

0.0.0.0:<CUSTOM_PORT>

then the configuration has been applied successfully.


Option 2: Keep ssh.socket and Change the Listening Port

If you prefer Ubuntu’s newer socket-activation approach, modify ssh.socket instead of sshd_config.

Create a Systemd Override

sudo mkdir -p /etc/systemd/system/ssh.socket.d
sudo nano /etc/systemd/system/ssh.socket.d/listen.conf

Add the following content:

[Socket]
ListenStream=
ListenStream=<CUSTOM_PORT>

Reload and Restart

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl status ssh.socket
sudo ss -ltnp | grep <CUSTOM_PORT>

Important Note

The following line is critical:

ListenStream=

It clears the default listening port before setting the new one.

Without this line, systemd may continue listening on both the default SSH port and the custom port simultaneously.

通过 WordPress.com 设计一个这样的站点
从这里开始