How readFlag Prevents Local Attacker Exploitation in Caveman

The readFlag function prevents local attacker exploitation by rejecting symbolic links, enforcing a 64-byte size limit, and validating content against a strict whitelist of known modes.

In the JuliusBrussee/caveman repository, readFlag serves as the secure counterpart to safeWriteFlag, reading the active-mode flag file (typically ~/.claude/.caveman-active) while mitigating risks from malicious local users. Without these safeguards, an attacker with write access to the flag directory could replace the file with a symbolic link pointing to sensitive data like ~/.ssh/id_rsa, causing the application to inadvertently exfiltrate private keys into model contexts or terminal output.

Understanding the Local Attacker Threat

A local attacker with sufficient filesystem permissions represents a significant threat to applications that blindly read configuration files. If readFlag processed the flag file without validation, a malicious user could execute:

fs.symlinkSync('/etc/passwd', path.join(os.homedir(), '.claude', '.caveman-active'));

When the application subsequently reads the flag, it would instead read the target of the symlink, potentially exposing system secrets or user credentials. This vector is particularly dangerous because compromised data might be passed to AI models or displayed in status lines, creating persistent information leaks.

Three-Layer Defense in readFlag

Located in src/hooks/caveman-config.js (lines 54-97), readFlag implements a defense-in-depth strategy through three sequential validation checks. Each check targets a specific exploitation technique, ensuring the function fails closed rather than risking data exposure.

The first line of defense uses fs.lstatSync() to inspect the file's metadata without following symbolic links. If isSymbolicLink() returns true, the function immediately returns null:

const stats = fs.lstatSync(flagPath);
if (stats.isSymbolicLink()) {
    return null; // Reject symbolic links entirely
}

This check prevents the flag file itself from being a pointer to arbitrary system files. According to the caveman source code, this validation occurs before any read operations, ensuring the function never dereferences attacker-controlled paths.

Size Limiting to Prevent Payload Injection

Even if the file is a regular file, readFlag enforces a maximum size of 64 bytes via the MAX_FLAG_BYTES constant:

if (stats.size > MAX_FLAG_BYTES) {
    return null; // Oversized files rejected
}

This limitation eliminates attempts to embed large payloads or binary data in the flag file. Since legitimate mode strings (such as off, lite, full, ultra, or wenyan-lite) require fewer than 12 bytes, the 64-byte cap provides ample headroom for valid use while blocking exfiltration attempts using large files.

Whitelist Validation for Mode Strings

After passing size and symlink checks, the raw file content undergoes strict validation against the VALID_MODES array:

const raw = fs.readFileSync(flagPath, 'utf8').toLowerCase().trim();
if (!VALID_MODES.includes(raw)) {
    return null; // Unknown mode strings rejected
}

The whitelist includes only recognized mode identifiers: off, lite, full, ultra, and wenyan-lite. Any unexpected content—including binary data, path traversals, or user-controlled strings—results in a null return. This guarantees that downstream consumers receive only trusted, pre-defined mode values.

Fail-Closed Security Design

When any validation check fails—whether from a missing file, symlink detection, size violation, or whitelist mismatch—readFlag returns null silently. This fail-closed design ensures that downstream code in src/plugins/opencode/plugin.js and src/hooks/caveman-statusline.ps1 interprets the result as "no active mode" rather than processing attacker-controlled data.

The test suite in tests/test_symlink_flag.js verifies this behavior, confirming that symbolic link attacks, oversized file injection, and invalid mode strings all result in immediate function termination without data exposure.

Practical Exploitation Scenarios

Consider an attacker attempting to exploit the system through various vectors:

Symlink Attack Neutralized:

const fs = require('fs');
const path = require('path');
const { readFlag } = require('./src/hooks/caveman-config');

const flag = path.join(os.homedir(), '.claude', '.caveman-active');
fs.symlinkSync('/etc/ssh/sshd_config', flag);

const result = readFlag(flag);
console.log(result); // → null (symlink rejected)

Oversized Payload Blocked:

fs.writeFileSync(flag, 'x'.repeat(200));
console.log(readFlag(flag)); // → null (size exceeds 64 bytes)

Invalid Mode Rejected:

fs.writeFileSync(flag, 'cat ~/.ssh/id_rsa');
console.log(readFlag(flag)); // → null (not in VALID_MODES)

These examples demonstrate that readFlag effectively neutralizes local attacker attempts to exfiltrate data through the flag file mechanism.

Summary

  • Symlink Rejection: readFlag uses fs.lstatSync().isSymbolicLink() to prevent the flag file from pointing to arbitrary system files.
  • Size Enforcement: A hard limit of 64 bytes (MAX_FLAG_BYTES) prevents large payload injection while accommodating legitimate short mode strings.
  • Strict Whitelisting: Only pre-defined modes (off, lite, full, ultra, wenyan-lite) are accepted; all other content returns null.
  • Fail-Closed Design: Any validation failure results in immediate null return, ensuring downstream components see "no active mode" rather than attacker-controlled data.
  • Implementation Location: The defensive logic resides in src/hooks/caveman-config.js lines 54-97, with comprehensive test coverage in tests/test_symlink_flag.js.

Frequently Asked Questions

The readFlag function detects the symbolic link using fs.lstatSync(flagPath).isSymbolicLink() and immediately returns null without reading the file content. This prevents the application from inadvertently reading and potentially exposing system password hashes or other sensitive files.

Why does readFlag enforce a 64-byte size limit on the flag file?

The 64-byte limit (MAX_FLAG_BYTES) prevents attackers from using the flag file to exfiltrate large amounts of data or embed malicious payloads. Since valid mode strings like wenyan-lite require fewer than 12 bytes, the 64-byte cap provides a generous safety margin while blocking attempts to read large files such as SSH private keys or configuration files.

Can readFlag be bypassed if the attacker controls the file content but not the file type?

No. Even if an attacker writes to a regular file (not a symlink) and keeps it under 64 bytes, readFlag validates the content against the VALID_MODES whitelist. Any content that does not exactly match off, lite, full, ultra, or wenyan-lite (after lowercasing and trimming) results in a null return, preventing arbitrary command injection or data exfiltration.

Where is the readFlag implementation located in the Caveman repository?

The readFlag function is implemented in src/hooks/caveman-config.js at lines 54-97. The repository also includes comprehensive security tests in tests/test_symlink_flag.js that verify symlink rejection, size limits, and whitelist validation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →