# Giving Codex access to my BIOS

A hardware experiment with GPT-6 Astra and NanoKVM: mixed RAM, failed boots, and the keyboard sequences that evolved along the way.

By rfalejo · September 13, 2026

[Read on rfalejo.dev](https://rfalejo.dev/writing/codex-gpt-6-astra-bios)

![Initial POST screen identifying the ASUS X470-F motherboard, Ryzen 7 2700X, 24 GB of RAM, and a safe-mode warning.](https://rfalejo.dev/images/bios-experiment/initial-post-safe-mode.jpg)

The starting point: 24 GB detected at DDR4-2666, with the firmware asking for setup after a safe-mode boot.

I connected Codex, running GPT-6 Astra, to a physical PC through NanoKVM. Together we sorted out mixed RAM, put back a stick I had written off as dead, and tried faster settings. All 32 GB booted at 3200 MT/s; the final stability test was still running when the session ended.

## The machine and the connection

My PC kept stopping in POST safe mode with mixed Corsair and Kingston RAM. I wanted it to boot reliably and run the memory at a sensible speed. A NanoKVM was already connected to the machine and exposed to Codex through MCP, so I asked Codex to investigate.

I was curious how far Codex could get with the same screen and keyboard access I used to navigate the BIOS.

> “We have a NanoKVM MCP attached so you can see the screen and pass keyboard inputs.”

*My opening instruction.*

I used Codex with GPT-6 Astra. I set the goal and answered its hardware questions, then moved the sticks when needed. Codex read screenshots and worked through the BIOS menus, changing settings and checking what happened after each reboot. The images here include screenshots from that conversation and my photographs of the hardware.

![The physical NanoKVM device with its illuminated status display and power and reset buttons.](https://rfalejo.dev/images/bios-experiment/nanokvm-device.jpg)

*The NanoKVM used for the session: the hardware connection between Codex and the target computer.*

NanoKVM has a [built-in MCP service](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/mcp.html). The tool schemas Codex saw match [Sipeed's implementation](https://github.com/sipeed/NanoKVM/blob/bd070b2f5ce27d22618e8a1c8d2bdca543429882/server/service/mcp/server.go). We used three tools:

| Tool | What it supplied to the session |
| --- | --- |
| `cube_screenshot` | A JPEG of the target display, with capture status and dimensions. |
| `cube_press_keys` | A key or simultaneous key combination, held briefly and then released. |
| `cube_type_text` | Text entered character by character through keyboard emulation. |

The connection name appears in calls such as `mcp__home_server_kvm__cube_press_keys`. The NanoKVM tool itself is `cube_press_keys`.

Sipeed documents [how to enable MCP](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/mcp.html#Enable-MCP) and [connect an AI tool using the endpoint and API key](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/mcp.html#Add-MCP-to-an-AI-Tool). Its configuration example uses OpenCode; this session used Codex.

NanoKVM [maps key names to USB HID keyboard reports](https://github.com/sipeed/NanoKVM/blob/bd070b2f5ce27d22618e8a1c8d2bdca543429882/server/service/mcp/keyboard.go). Its [input executor](https://github.com/sipeed/NanoKVM/blob/bd070b2f5ce27d22618e8a1c8d2bdca543429882/server/service/mcp/remote.go) sends a key-down report and then releases the keys. Text entry is a sequence of these reports, which lets Codex type numeric values into firmware without an operating-system clipboard.

Codex could capture the screen, send input, and inspect the result before the operating system loaded. NanoKVM had no command to set a memory frequency. Codex had to find the setting on screen and reach it through the keyboard.

## Finding the fourth stick

Codex started by identifying the hardware in the firmware screens: an ASUS ROG STRIX X470-F GAMING running BIOS 6232, a Ryzen 7 2700X, and 8 GB modules from Corsair and Kingston. The Corsair part number was `CMK16GX4M2Z3200C16`; the Kingston modules reported `KHX3200C16D4/8GX`.

Both kits had a 3200 profile with 16-18-18-36 primary timings at 1.35 V. The machine was running below that speed. A stored profile told us what to try; we still had to find out whether the system would start with it.

### Compare the two RAM profiles

![ASUS SPD information showing the Corsair module identity, its 3200 profile at 1.35 V, and current memory operation at 2666.](https://rfalejo.dev/images/bios-experiment/corsair-spd-profile.jpg)

Reading the module's profile and the current operating state from the same screen.

![Kingston SPD information showing the KHX3200C16D4/8GX module and its 3200 profile at 16-18-18-36 and 1.35 V.](https://rfalejo.dev/images/bios-experiment/kingston-spd-profile.jpg)

The Kingston module exposes the same primary timings and voltage for its 3200 profile. Its default JEDEC profile differs from the Corsair module’s, so the matching advertised speed does not make the modules identical.

An empty slot made Codex suspect an undetected module. I explained that only three sticks were installed. We rearranged the Kingston pair into A2/B2, then checked the slots in BIOS.

![Close-up of the installed memory modules, an empty DIMM slot, and the motherboard’s printed slot labels.](https://rfalejo.dev/images/bios-experiment/physical-dimms.jpg)

*The three installed modules, with A1 empty. This photograph is what Codex read; the slot check happened in firmware.*

Codex misread one of my photographs and placed the Corsair in A1. The firmware showed it in B1, with A1 empty, and Codex corrected itself. That check also told us which slot was free.

> “BIOS shows Corsair in B1 and A1 empty, so my photo-based slot mapping was incorrect; the matched pair is still where we want it.”

*Codex correcting its slot identification.*

### Checking the slot identities

![ASUS SPD information with DIMM_B1 selected and an 8 GB Corsair module detected.](https://rfalejo.dev/images/bios-experiment/corsair-detected-in-b1.jpg)

Firmware identifies the Corsair module in B1 after the physical rearrangement.

![ASUS SPD information with DIMM_A1 selected and no module details displayed.](https://rfalejo.dev/images/bios-experiment/a1-slot-empty.jpg)

Checking A1 shows no module information. Together, these screens supplied the evidence for correcting the photo-based slot mapping.

> “I have the other Corsair stick lying around because I assumed it got bad…”

*My message before reinstalling the spare.*

I powered down and installed the spare. The machine booted with 32 GB, and Windows showed all four slots populated.

> “It booted! Please run the memory test again”

*My reply after installing the spare.*

![Windows Task Manager showing 32 GB DDR4, 2666 MT/s, and four of four memory slots occupied.](https://rfalejo.dev/images/bios-experiment/windows-32gb-detected.jpg)

*After I reinstalled the spare module, Windows detected all 32 GB.*

## Testing across reboots

I offered to sign into Windows so it could use Windows Memory Diagnostic. Everything we needed was already on the machine.

The same NanoKVM connection worked throughout. Codex opened Windows Memory Diagnostic from Start, checked the restart dialog, and activated the selected option. After the reboot, it captured the diagnostic screen.

We launched the diagnostic three times: with 24 GB at 2666 MT/s, with 32 GB at 2666 after I installed the spare, and finally with 32 GB at 3200. I interrupted the first run to install the stick and reported success after the second.

Codex used this keyboard sequence:

```text
Ctrl+Esc → open Start
Type "Windows Memory Diagnostic"
Enter → open the utility
Capture the screen → inspect the selected restart option
Enter → activate "Restart now and check for problems"
Wait through reboot
Capture the screen → confirm the preboot test is running
```

The screenshots below follow the second launch, with all 32 GB detected at the 2666 baseline.

### Starting Windows Memory Diagnostic

![Windows Start search with Windows Memory Diagnostic selected after Codex typed its name.](https://rfalejo.dev/images/bios-experiment/windows-memory-diagnostic-search.jpg)

Codex opened Start with Ctrl+Esc and typed the utility's name. Windows selected the matching application.

![Windows Memory Diagnostic dialog with Restart now and check for problems selected.](https://rfalejo.dev/images/bios-experiment/windows-memory-diagnostic-restart.jpg)

The restart option had keyboard focus. After inspecting this screen, Codex pressed Enter to activate it.

![Preboot Windows Memory Diagnostic running the first of two passes at 0% after the second launch.](https://rfalejo.dev/images/bios-experiment/windows-baseline-test-running.jpg)

After reboot, the same NanoKVM connection captured the diagnostic running outside the Windows desktop. This frame confirms the test started; my report of success came later.

The first attempt to launch the diagnostic hit an input-ownership problem: Ctrl+Esc and text entry were rejected with `manual control is active`. Later retries worked. Slower keystrokes alone cannot explain that recovery; the control path also had to become available.

## Trying faster settings

I asked Codex to try faster settings, including 3600. It started at the modules' rated 3200 profile, with the rated timings and voltage. All 32 GB passed POST.

> “Let's try several combinations until one of them gets us the 3600mhz”

*My request to keep experimenting.*

We tried four configurations at 3600 and three at 3400. All seven failed memory training. Codex varied the primary timings, command rate, Gear Down Mode, and SoC voltage. The board recovered into safe mode after failures, sometimes falling back to 2400.

### The first 3600 trial: settings and recovery

![BIOS save confirmation for the first 3600 trial, changing frequency from 3200 to 3600 and timings from 16-18-18-36 to 18-22-22-42.](https://rfalejo.dev/images/bios-experiment/save-3600-trial.jpg)

Before the first 3600 trial: the save dialog lists the proposed frequency and timing changes. It does not establish that the machine can boot with them.

![POST recovery screen reporting 32768 MB at DDR4-2400 and a safe-mode warning after the first 3600 trial.](https://rfalejo.dev/images/bios-experiment/3600-trial-safe-mode.jpg)

After that trial: all 32 GB are detected, but the board has recovered into safe mode at 2400 rather than starting at the requested 3600.

> “All four 3600 configurations tried so far have failed POST, including loose timings, Gear Down Mode, and 2T with 1.10 V SoC.”

*Codex reporting the failed 3600 trials.*

Between the 3600 and 3400 trials, I asked Codex to look for other people's results with the same modules. Before it started searching, it noted what was still pending in BIOS:

> “The BIOS is still on the frequency menu; the 3200 restoration has not been saved yet.”

It had said it would restore 3200, but had not saved the change.

One promising report needed a closer read:

> “The closest Kingston report initially claimed 3800, then reported poor performance; I’m checking its final result before treating it as a success.”

The reports involved different CPUs, matched kits, or module revisions. Codex found no verified success with my exact four-stick configuration.

Codex asked for the Corsair revision printed on the modules. I checked both sticks: version 4.31. I then asked it to try 3400.

## How the keyboard sequences evolved

The tool defaults were a 50 ms key hold and 30 ms between typed characters. By the end of the session, Codex was requesting 100 to 200 ms holds and 200 ms typing delays, with explicit waits between steps.

The excerpts below follow that change in order. I reformatted them for readability and ended the later excerpts before screenshot handling or further menu navigation; the keystrokes, values, and delays are unchanged. They come from different starting screens, so this is a chronological comparison, not a controlled test. The effective sequences are my expansion of what the code requested, not USB captures.

A `keys` array sends a simultaneous combination: `ControlLeft`, `AltLeft`, and `Delete` together mean Ctrl+Alt+Delete. Separate arrow presses need separate calls, hence the loops.

### Finding a route through the menus

An early call moved down three entries and pressed Enter:

```js
for (let i = 0; i < 3; i++)
  await tools.mcp__home_server_kvm__cube_press_keys({ keys: ["ArrowDown"] });
await tools.mcp__home_server_kvm__cube_press_keys({ keys: ["Enter"] });
```

Effective key sequence:

```text
Down → Down → Down → Enter
```

Arrows show order, and explicit waits are listed separately from key holds. With no hold duration specified, these presses use the schema's 50 ms default, and there are no waits between them.

A later recovery sequence entered setup with F1, waited, moved right, then stepped down eleven entries with pauses before pressing Enter. The original call requested a screenshot next:

```js
await tools.mcp__home_server_kvm__cube_press_keys({ keys: ["F1"], holdMs: 100 });
await new Promise((r) => setTimeout(r, 1800));
await tools.mcp__home_server_kvm__cube_press_keys({
  keys: ["ArrowRight"],
  holdMs: 100,
});
await new Promise((r) => setTimeout(r, 700));
for (let i = 0; i < 11; i++) {
  await tools.mcp__home_server_kvm__cube_press_keys({
    keys: ["ArrowDown"],
    holdMs: 100,
  });
  await new Promise((r) => setTimeout(r, 250));
}
await tools.mcp__home_server_kvm__cube_press_keys({ keys: ["Enter"], holdMs: 200 });
await new Promise((r) => setTimeout(r, 1500));
```

Effective key sequence:

```text
F1 [hold 100 ms] → wait 1.8 s
Right [hold 100 ms] → wait 700 ms
(Down [hold 100 ms] → wait 250 ms) × 11
Enter [hold 200 ms] → wait 1.5 s
```

By then, Codex was following a route with time for the interface to respond at each step. It still needed to start from the expected screen; eleven Down presses would mean something else in another menu.

### Getting back into firmware

An earlier call had requested a 1.5-second Delete hold:

```js
await tools.mcp__home_server_kvm__cube_press_keys({
  keys: ["Delete"],
  holdMs: 1500,
});
```

Effective key sequence:

```text
Requested: Delete [hold 1.5 s]
Actual: validation error; no key press executed
```

The tool rejected the request because `holdMs` exceeded its 1,000 ms maximum, also listed in the [NanoKVM tool schema](https://github.com/sipeed/NanoKVM/blob/bd070b2f5ce27d22618e8a1c8d2bdca543429882/server/service/mcp/server.go#L408). No key press ran. Later, Codex pressed Enter to confirm a restart and sent fifteen short Delete presses, waiting 1.5 seconds before each. It then requested a screenshot to see where the machine had landed:

```js
await tools.mcp__home_server_kvm__cube_press_keys({ keys: ["Enter"], holdMs: 100 });
for (let i = 0; i < 15; i++) {
  await new Promise((r) => setTimeout(r, 1500));
  await tools.mcp__home_server_kvm__cube_press_keys({
    keys: ["Delete"],
    holdMs: 100,
  });
}
```

Effective key sequence:

```text
Enter [hold 100 ms]
(wait 1.5 s → Delete [hold 100 ms]) × 15
```

Each wait comes before its Delete press, including the first. The screenshot comes after the fifteenth press.

The earlier call failed at API validation. In the later routine, Codex used 100 ms holds and put the longer waits in the orchestration code. The progression is visible, although the record doesn't show whether GPT-6 Astra connected that choice to the earlier error.

### Reusing the timing-entry loop

For a 3400 trial, Codex filled successive timing fields with this loop:

```js
for (const v of ["18", "20", "20", "20", "40"]) {
  await tools.mcp__home_server_kvm__cube_press_keys({
    keys: ["ArrowDown"],
    holdMs: 100,
  });
  await new Promise((r) => setTimeout(r, 400));
  await tools.mcp__home_server_kvm__cube_type_text({ text: v, delayMs: 200 });
  await tools.mcp__home_server_kvm__cube_press_keys({
    keys: ["Enter"],
    holdMs: 100,
  });
  await new Promise((r) => setTimeout(r, 400));
}
```

Effective key sequence:

```text
Down → type "18" → Enter
Down → type "20" → Enter
Down → type "20" → Enter
Down → type "20" → Enter
Down → type "40" → Enter
```

Each row is a 100 ms Down press, a 400 ms wait, the value typed with `delayMs: 200`, a 100 ms Enter press, and another 400 ms wait.

### Inspect the five timing fields

![BIOS timing controls showing entered values of 18, 20, 20, 20, and 40 for the first 3400 trial.](https://rfalejo.dev/images/bios-experiment/3400-trial-timing-fields.jpg)

*The five fields populated by the loop, including separate read and write RCD controls. These are the entered settings before reboot; the hardware monitor still reports the current fallback speed of 2400.*

To restore 3200, Codex ran the same loop with 16, 18, 18, 18, 36. Only the values changed; the field order and timings stayed the same, including the five separate entries this BIOS needs for split read and write RCD controls.

This is the pattern I could see in the conversation: Codex accumulated routes through the menus and reused input procedures on this machine. It still took screenshots and sometimes sent individual corrective keys between longer sequences; those checks showed whether a setting had changed or a reboot had worked.

I'd call this in-context adaptation. Earlier observations in the conversation guided later decisions, which is what the [in-context learning](https://arxiv.org/abs/2005.14165) paper describes: adapting without a training step. Reading labels and choosing actions from screenshots worked in this firmware interface too. OpenAI's [computer-use documentation](https://developers.openai.com/api/docs/guides/tools-computer-use) describes the same action-and-screenshot loop; NanoKVM provided the screen and input paths here.

What the conversation doesn't show is what GPT-6 Astra already knew. It may have known shortcuts such as F10 or Ctrl+Shift+Esc going in, and I can't tell whether this BIOS appeared in its training or whether it kept any lasting skill from the session. The longer routines alone don't prove that navigation got faster or more reliable. The screenshots do show Codex operating the firmware through general screen and keyboard tools.

## Results and usage

After the faster settings failed, Codex returned to the 3200 configuration that had booted earlier. The save dialog listed the rollback from 3400 to 3200, tighter primary timings, and Gear Down Mode back on Auto.

### Inspect the rollback confirmation

![BIOS save confirmation listing the rollback from DDR4-3400 to DDR4-3200 and restoration of the primary timings.](https://rfalejo.dev/images/bios-experiment/restore-3200-settings.jpg)

*The firmware's own change summary before saving the rollback.*

BIOS then reported 32768 MB at 3200 and 1.350 V. Windows booted, and Codex started another two-pass memory diagnostic.

### Verify the restored memory configuration

![BIOS main screen showing the Ryzen 7 2700X, 32768 MB of memory, and memory operation at 3200 and 1.350 V.](https://rfalejo.dev/images/bios-experiment/bios-32gb-at-3200.jpg)

*All four modules detected after the return to 3200 MT/s.*

The session ends with the diagnostic at 1% of its first pass and no problems detected yet. We don't have a completed test at 3200:

> “The two-pass memory test is now running. **No errors so far; stability is not yet confirmed.**”

*The final recorded response.*

![Windows Memory Diagnostic running pass one of two at 1%, with no problems detected yet.](https://rfalejo.dev/images/bios-experiment/final-memory-test-running.jpg)

*Pass one of two at 1% when the session ended.*

We went from three detected modules to four after I installed the spare, and I reported a passing baseline test at 2666. After seven failed attempts at higher speeds, Codex restored a 3200 configuration that booted. We hadn't established the fastest stable setting or measured an improvement in application performance.

Across the three saved transcripts, I counted 232 model responses totaling 29,935,286 tokens:

| Token category | Recorded tokens |
| --- | ---: |
| Cached input | 28,941,824 |
| Uncached input | 969,703 |
| Output | 23,759 |
| **Total** | **29,935,286** |

About 96.8% of the input was cached. The total counts processing across responses, including repeated conversation context and image inputs. It isn't 30 million tokens of unique material, and it doesn't tell us the dollar cost.

There were also 901 NanoKVM MCP calls, counting each invocation inside a loop:

| MCP tool | Calls | Completed | Failed |
| --- | ---: | ---: | ---: |
| `cube_press_keys` | 751 | 628 | 123 |
| `cube_screenshot` | 119 | 116 | 3 |
| `cube_type_text` | 31 | 30 | 1 |
| **Total** | **901** | **774** | **127** |

A keyboard call can send a combination, while a typing call can enter a whole string, so 901 calls doesn't mean 901 keystrokes. A completed call only tells us the tool finished; we still needed screenshots and the machine's subsequent behavior to check the result.

The 127 failed calls include rejected arguments, unavailable input ownership, and three failed screenshots.

### How I counted

Tokens: I summed the usage records after deduplicating them by response ID. The final thread counter reports 29,819,091 tokens. Another 116,195 tokens belong to an earlier continuation outside that final counter, which brings the all-files total to 29,935,286.

MCP calls: I counted the tool events and deduplicated them by call ID.

## Who did what

I handled the sticks and supplied information the screen couldn't show. Codex did much of the repetitive navigation and checking, including the work before the operating system loaded. Through NanoKVM, we could use the same process of acting and checking in an older firmware interface that we used on the Windows desktop.

## About my work

I'm a Principal Software Architect working on C#/.NET backends, distributed systems, and AI agent integrations. Experiments like this are how I explore where software meets hardware.
