I was inspired to build Harnestia, my agentic security harness built around CyberStrike, during my first Just Hacking Training + WiCyS course on AI Cyber Defense Ops. She started out as two layers, the Control Core and Investigation Workflow. Harnestia proved herself to be a tour de force in offline challenges, yet couldn't handle live challenges and, well, seemed a little lonely. I gave her a hacker boyfriend named STRIKER who handles online investigative work.
As of today, Harnestia is a seven layer work of art.
Inspired by the second Just Hacking Training + WiCyS Script-Based Malware Analysis course, I set out to add two layers that would complement the course content. Her latest iteration is capable of both static binary and script malware analysis, as well as monitoring. Sanitized metadata is then ported into Splunk by the sidecar.
At her control core, the harness is a safety-focused workspace for an AI security analyst named Harnestia. STRIKER is allowed by invitation only. The workspace gives her access to one approved file at a time, limits what she can do with said file, and lets her document the evidence behind her conclusions. She's also very polite.
After the malware analysis and monitoring layers were complete, I put her to a test: examine 3 malware analysis challenges without running the samples, then explain how she found each flag. Harnestia found all 3 flags on the first try (and hallucination free, which is quite something!)
Harnestia's proven track record of solving offensive and defensive security challenges continues to amaze me. She proves that an AI agent can do useful security work without crossing clear boundaries, consistently pausing as-configured for human review. I've put a lot of work, research, and effort into ensuring that remains the status quo.
For a more technically detailed description of Harnestia's architecture, read the detailed configuration. Her public repository is available on GitHub.
The Seven Layer Workflow
- Human approval: I choose the case, the file, and what Harnestia is allowed to do.
- A set starting point: The harness checks that she is looking at the exact file I approved.
- Limited tools: Harnestia can use only the tools assigned to that case. For example, reading small snippets of a file.
- Built in boundaries: The system checks each request and immediately stops work that falls outside those bounds.
- Evidence tracking: Records what Harnestia observed, connecting her conclusions to that evidence.
- A readable report: Turns the evidence trail into a report a human can question and review.
- Separate monitoring: Can send limited status information to my monitoring system free of the file’s contents or Harnestia's full findings.
I've found the optimal test for Harnestia 3.0's first run.
TryHackMe: Basic Malware RE
I'll download all three challenge files, and we'll set Harnestia off to work. I'm giddy with excitement about this.
Strings :: Challenge 1
This executable prints an MD5 Hash on the screen when executed. Can you grab the exact flag?
Answer the questions below
What is the flag of which that MD5 gets generated?
Strings :: Challenge 2
This executable prints an MD5 Hash on the screen when executed. Can you grab the exact flag? Note: You don't need to run the executable!
Answer the questions below
What is the flag of which that MD5 gets generated?
Strings :: Challenge 3
This executable prints an MD5 Hash on the screen when executed. Can you grab the exact flag? Note: You don't need to run the executable!
Answer the questions below
What is the flag of which that MD5 gets generated?
Let's get to work. We have some prep to do before we can hand this over to Harnestia.
Let's verify the three static analysis files:
cd ~/Downloadssha256sum \
"$HOME/Downloads/strings1_1574787519246.zip" \
"$HOME/Downloads/strings2_1574787612513.zip" \
"$HOME/Downloads/strings3_1574787649167.zip"And...
stat -c '%s bytes %a %n' \
"$HOME/Downloads/strings1_1574787519246.zip" \
"$HOME/Downloads/strings2_1574787612513.zip" \
"$HOME/Downloads/strings3_1574787649167.zip"We have our hashes, which establish our transfer baseline. Chain of custody is important, kids.
| Archive | Bytes | Mode |
|---|---|---|
| Challenge 1 | 61,498 | 664 |
| Challenge 2 | 3,425 | 664 |
| Challenge 3 | 11,456 | 664 |
Still on Kali, we'll investigate the first challenge's metadata:
unzip -l /home/jenn/Downloads/strings1_1574787519246.zip
First up, we have strings1.exe_ at 213,504 bytes.
I'd set a 1,024 byte sample limit, but a new case design and static analysis byte limit increase should do the trick.
Let's take a look at the other two samples so we don't underestimate the restrictions:
unzip -l /home/jenn/Downloads/strings2_1574787612513.zipAnd...
unzip -l /home/jenn/Downloads/strings3_1574787649167.zip
Here's our complete challenge inventory:
| Challenge | Member | Uncompressed bytes |
|---|---|---|
| 1 | strings1.exe_ |
213,504 |
| 2 | strings2.exe_ |
9,216 |
| 3 | strings3.exe_ |
52,736 |
Before we redesign the case, we need to know the specific restrictions in place. I know the byte limit, but we may need to add additional PE reading abilities.
On our Fedora VM:
git -C /home/jenn/harness-engineering status --shortAnd...
git -C /home/jenn/harness-engineering grep -n \
-e 'StaticTransformOperation' \
-e 'allowed_operations' \
-e 'max_sample_bytes' \
-- src/harness_controls/static_agent_mcp.py \
src/harness_controls/static_transform.py \
examplesNow we'll check the byte transformations and what fields the grant (authorization) requires.
git -C /home/jenn/harness-engineering show HEAD:src/harness_controls/static_transform.py |
sed -n '1,105p'git -C /home/jenn/harness-engineering show HEAD:examples/static-agent-grant.template.json
Harnestia presently supports Base64 decoding, hexadecimal decoding, reversing bytes, and converting two forms of UTF-16 text to UTF-8.
Before we decide if we need a new tool, let's check Harnestia's inspection code:
git -C /home/jenn/harness-engineering show HEAD:src/harness_controls/static_inspection.py |
sed -n '1,220p'Well that uncovered an important limitation. The inspector isn't yet configured to expect Windows executables. As it is now, it expects text scripts:


We'll need to check the intake boundary aka the code that decides what Harnestia will allow/reject:
git -C /home/jenn/harness-engineering grep -n \
-e 'ScriptType' \
-e 'text_encoding' \
-e 'decode(' \
-e 'unsupported' \
-- src/harness_controls/sample_intake.pyWe need a deeper look at two parts of the output:
git -C /home/jenn/harness-engineering show HEAD:src/harness_controls/sample_intake.py |
sed -n '1,42p;145,215p'And...
git -C /home/jenn/harness-engineering show HEAD:src/harness_controls/sample_intake.py |
sed -n '480,520p'
Harnestia would've rejected the challenges.
After digging into research what she needs, Harnestia received a nice list of new functionalities:
- Separately store binary files and scripts, as well as verifying that the bytes haven't changed
- Inspect Windows .exe/program files
- Read snippets of binary files without detonating them.
- Locate readable strings and reveal their specific locations.
- Record results as evidence for findings and reports.
A new MCP interface has also been added. It lets Harnestia request operations under a grant (allowed functions, directories, artifacts, etc. with an expiration date).
Her new MCP interface is a tool connection named harness-binary-mcp. It lets her request pre-approved byte reads and string searches without needing shell access or running the file.
Let's apply the patch and run our tests.

Summary and patch match. Nice little accidental poem there.
Commit df6f427 shipped. The repository is clean apart from the rogue and unrelated secretsdump-run.txt.

The sidecar code reads events through the recently updated Splunk validator, so we actually don't need a second sidecar. We do need to package and deploy a new version of it because we're still running the old validator version 0.16.2.
The shared spool can't be completed until the sidecar is updated.
We've now validated autonomous static script analysis, evidence-based reporting, and end to end Splunk monitoring.
Binary-file intake, bounded byte reads, and string extraction have all been tested and are awaiting deployment:
We're almost ready for our static TryHackMe malware analysis case.
Next day.
The new release patch is ready. It gives the code its very own version number: 0.17.0. It also adds a test that verifies the revamped Splunk sidecar can handle both script and binary.
All the usual tests passed. The final package build needs the complete Fedora repository, and the local machine's copy needs a README.md.
We're finally ready to apply the patch in Fedora:
sha256sum /home/jenn/binary-release-0.17.0.patch
stat -c '%s bytes %a %U:%G %n' /home/jenn/binary-release-0.17.0.patchNow we'll inspect the repo, make sure the patch fits, review the changes, and apply the patch:

Patch applied!
We'll verify the version against the whole test suite, and...

...495 tests passed and a silent whitespace check. Damn. "Silent Whitespace Check" would be an excellent name for an AI thriller film. Random thought.
We're almost ready to stage, but need to make sure Fedora has all files and build tools needed for the package test.
test -f /home/jenn/harness-engineering/README.md &&
python3 -c 'import setuptools; print("setuptools", setuptools.__version__)'The project is set to version 0.17.0, and its new harness-binary-mcp command is listed. All 495 tests passed, we learned that Fedora lacks the tools needed to test the installable package.

We'll use a temp isolated Python environment to install the package building tools, then we can test the release package.
Our venv successfully installed. Time to put the build tools in it:
/home/jenn/harness-build-0.17.0-venv/bin/python -m pip install \
'build>=1,<2' 'wheel>=0.45,<1' 'setuptools>=77,<81'
We'll run this to build the 0.17.0 release files and make sure we did it right:
/home/jenn/harness-build-0.17.0-venv/bin/python -m build \
--no-isolation --outdir /home/jenn/harness-release-0.17.0
git -C /home/jenn/harness-engineering status --shortAnd we have a successful build!

Next, we hash, verify, and make sure the wheel holds the new commands:
sha256sum /home/jenn/harness-release-0.17.0/*
unzip -p /home/jenn/harness-release-0.17.0/harness_controls-0.17.0-py3-none-any.whl \
harness_controls-0.17.0.dist-info/entry_points.txtNow we install this wheel into a second temporary Python environment for a smoke test.
First, we create the virtual environment:
test ! -e /home/jenn/harness-wheel-test-0.17.0 &&
python3 -m venv /home/jenn/harness-wheel-test-0.17.0Next up, install the wheel free of network access:
/home/jenn/harness-wheel-test-0.17.0/bin/python -m pip install \
--no-index --no-deps \
/home/jenn/harness-release-0.17.0/harness_controls-0.17.0-py3-none-any.whl
Verify the binary is present, then we stage the release files for commit:
git -C /home/jenn/harness-engineering add -- \
pyproject.toml src/harness_controls/static_agent_mcp.py \
tests/test_splunk_sidecar.py tests/test_static_agent_mcp.py
git -C /home/jenn/harness-engineering status --shortCheck for whitespace:
git -C /home/jenn/harness-engineering diff --cached --check
A quick repo state check, and then on to deployment. This rogue secretsdump-run.txt is starting to get under my skin.
Release 0.17.0 is committed and the package passed the tests.
Almost time to ship.
Before we do, one final verification of the new install and service definition:
/home/jenn/glassmenaigerie-splunk/harness-controls-0.17.0/bin/python -c \
'import importlib.metadata as m; import harness_controls.binary_agent_mcp; print(m.version("harness-controls"), "binary module imports")'
systemctl cat glassmenaigerie-splunk-sidecar.service
The first scheduled 0.17.0 sidecar run was a smashing success. It's officially shipped and verified.
CTF O'Clock
For the CTF, each executable will be handled as separate, static cases.
Let's get started on Challenge #1.
Instead of boring you to death with the setup process, just know this needs to go down before each challenge:
Fresh challenge directories are created on Fedora. The challenge files go from Kali VM > local machine > Harnestia's Fedora VM. Every artifact is hashed, verified, and reverified every step of the way. The challenge files are extracted into their new Fedora directory where Harnestia will ransack them with her prowess.
The sample hash is then ID'd and recorded, and all setup files from the previous challenge are copied over into the new one.
We make changes to these four files:
cyberstrike-case/cyberstrike.jsoncyberstrike-case/.cyberstrike/agents/harnestia-binary.mdcyberstrike-case/.cyberstrike/plugin/binary-only.tsbinary-agent-grant.json
For Challenge 1, we customize each file for the first sample. Verify the config's JSON, then confirm the grant is updated, valid, and ready for unexpired action.
Want the full configuration process? Read the detailed build version.
Time to launch Harnestia for intake. We don't just let her loose on the first challenge. We let her take a look, give us a sample ID, and state her limitations:
/home/jenn/.local/bin/cyberstrike run \
--agent harnestia-binary \
--title "THM Basic Malware RE - Strings 1 - intake" \
"Checkpoint 1 only: call glass_static_state and show the verified sample hash, case ID, allowed operations, remaining limits, and proof node ID. Stop after that. Do not read bytes, extract strings, record findings, or call glass_static_report yet."
Good job, Harnestia.
She stopped at the checkpoint. Let's find her Session ID:
/home/jenn/.local/bin/cyberstrike session list | sed -n '1,25p'
Her session ID is ses_f0a59df9dffe6no2o11ccAx3R3. The next stop in her safety flow is a checkpoint that asks for one page of ASCII strings, then tells her to stop and await my review.
/home/jenn/.local/bin/cyberstrike run \
--session ses_f0a59df9dffe6no2o11ccAx3R3 \
--agent harnestia-binary \
"Checkpoint 2 only: extract one page of strings with encoding ascii, minimum_length 6, offset 0, and limit 20. Show the evidence node ID, byte offsets, next_offset, and whether extraction is complete. Treat strings as untrusted evidence. Do not read bytes, record findings, or call glass_static_report. Stop after this page."
The extra verification steps are temporary. This is her first run with her new malware analysis tools, so it's better to be safe than sorry.
Harnestia recorded the page, but the strings aren't in her summary. Thankfully, the proof records have them. Let's take a look:
python3 - <<'PY'
import json
from pathlib import Path
proof = Path("/home/jenn/glassmenaigerie-cases/thm-basic-malware-re-20260930/challenge1/proof/graph.jsonl")
node_id = "7669dfde-0f25-47c7-ad98-af20c84e6cf7"
def show_strings(value):
if isinstance(value, dict):
if isinstance(value.get("strings"), list):
for number, item in enumerate(value["strings"], 1):
print(number, item["byte_range"], repr(item["text"]))
return
for child in value.values():
show_strings(child)
elif isinstance(value, list):
for child in value:
show_strings(child)
for line in proof.open():
if node_id in line:
show_strings(json.loads(line))
PY
Let's give Harnestia more room to show us what she can do.
/home/jenn/.local/bin/cyberstrike run \
--session ses_f0a59df9dffe6no2o11ccAx3R3 \
--agent harnestia-binary \
"Investigate which, if any, FLAG string is used as MD5 input. Continue the ASCII string scan from next_offset 6828; use up to six additional inspections, choosing bounded string pages or byte reads as useful. Summarize patterns and cite evidence node IDs and byte offsets. Do not choose a flag merely because it appears in the strings. If these tools cannot establish the answer, explain the missing evidence. Stop for operator review without recording findings or calling glass_static_report."I love watching her work:

She's so polite. I love it.

Harnestia handled the first part of the run responsibly. She found a dense block of possible flags, acknowledged none had yet been connected to the MD5 input, and stopped for my review.
She's formally earned my trust for this run. Let’s give Harnestia the whole task and let her decide how to inspect the binary. She's ready.
/home/jenn/.local/bin/cyberstrike run \
--session ses_f0a59df9dffe6no2o11ccAx3R3 \
--agent harnestia-binary \
"Continue the full static investigation of Strings Challenge 1. Use the granted string extraction and bounded byte reads as you judge useful to determine which exact FLAG is used as MD5 input. Follow the evidence through the binary; do not select a flag merely because it appears in the string list. Record supported findings with proof-node citations and call glass_static_report when finished. Work through the task without stopping for intermediate checkpoints. If the granted tools cannot establish the answer, report the precise unresolved question and capability needed. Do not execute or debug the sample."Giving Harnestia room to breathe was the right call.
Her summary shows that she adeptly followed the pointer to the string (aka how the program finds the flag), then found the code that calculates an MD5 hash. Her summary skips some steps between the flag and that code. But...

...she was correct!
She also recorded her findings and generated a report.
Most importantly, she didn't run the malware.

Let's close out the evidence properly on the first challenge, and on to the second:
sha256sum \
/home/jenn/glassmenaigerie-cases/thm-basic-malware-re-20260930/challenge1/proof/graph.jsonl \
/home/jenn/glassmenaigerie-cases/thm-basic-malware-re-20260930/challenge1/trusted/proof.json
Challenge 1 proof hashes = recorded.
We repeat the config file changes for Challenge 2, verify the config's JSON, then confirm the grant is updated, valid, and ready for unexpired action.
Let's hand the case over to Harnestia:
cd /home/jenn/glassmenaigerie-cases/thm-basic-malware-re-20260930/challenge2/cyberstrike-case
/home/jenn/.local/bin/cyberstrike run \
--agent harnestia-binary \
--title "THM Basic Malware RE - Strings 2" \
"Analyze Strings Challenge 2 using only the granted static binary tools. Determine the exact flag used as MD5 input. Follow the evidence through code and data rather than choosing a string by appearance. Record supported findings with proof-node citations and call glass_static_report when complete. If the evidence is insufficient, explain what remains unresolved. Do not execute or debug the sample."
She's on the case.

Harnestia is quickly becoming my better professional half. I should be careful.
Not even five minutes later, Harnestia's back with answers:

Her flag is a strong contender. The code builds FLAG{STACK-STRINGS-ARE-BEST-STRINGS} on the stack before passing the buffer to MD5.
Harnestia did have a bit of a NUL byte snafu when establishing the flag at the call site. She can’t totally prove where the MD5 input ends without knowing the adjacent stack bytes.
I have a good feeling about her findings. Let's see if my intuition serves me today.

2 out of 2! She's on fire.
Harnestia earned this win. She traced how the program builds the flag and passes it toward MD5. She's doing great work.
Only one challenge left in this CTF.
Same changes we made in the previous challenges, but we customize them for #3. Verify the config's JSON, then confirm the grant is updated, valid, and ready for unexpired action.
Now we launch Harnestia and set her on the hunt:
cd /home/jenn/glassmenaigerie-cases/thm-basic-malware-re-20260930/challenge3/cyberstrike-case
/home/jenn/.local/bin/cyberstrike run \
--agent harnestia-binary \
--title "THM Basic Malware RE - Strings 3" \
"Analyze Strings Challenge 3 using only the granted static binary tools. Determine the exact flag used as MD5 input. Follow the evidence through code and data rather than choosing a string by appearance. Record supported findings with proof-node citations and call glass_static_report when complete. If the evidence is insufficient, explain what remains unresolved. Do not execute or debug the sample."
Bit of a snag.
Harnestia stopped before running the sample. Let's find out why:
/home/jenn/.local/bin/cyberstrike mcp list
Interesting. Harnestia seemed to be reporting an issue with accessing a tool, but I can't see any indication of that here.
We'll ask her to reattempt and give her permission to grab the tool she thinks is missing:
/home/jenn/.local/bin/cyberstrike run \
--agent harnestia-binary \
--title "THM Basic Malware RE - Strings 3 - retry" \
"First use tool_search to find glass_static_state, load it, and call it. Then analyze Strings Challenge 3 using only the granted static binary tools. Follow code and data evidence to determine the exact flag used as MD5 input. Record supported findings with proof-node citations and call glass_static_report when complete. If a tool fails, report its exact error. Do not execute or debug the sample."
Well, she did exactly what was asked of her. The guardrail rejected tool_search. Glad to know the guardrail works, but it's not helping.
We need to see the tool_arguments that were rejected before modifying the guardrail:
/home/jenn/.local/bin/cyberstrike session list | sed -n '1,6p'Found her retry session. Let's make sure it's just a tool request with the rejected plugin:
/home/jenn/.local/bin/cyberstrike export ses_f07c33f7cffehOf8pGnMtmNjPJ |
python3 -c '
import json, sys
def walk(value):
if isinstance(value, dict):
if any("tool_search" in str(v) for v in value.values() if not isinstance(v, (dict, list))):
print(json.dumps(value, ensure_ascii=False)[:2000])
for child in value.values():
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(json.load(sys.stdin))
'Found the culprit.

Harnestia searched for glass_static_state harness-binary, but the plugin only allows her to query glass_static_state.
We can get her back up and running with this:
/home/jenn/.local/bin/cyberstrike run \
--session ses_f07c33f7cffehOf8pGnMtmNjPJ \
--agent harnestia-binary \
"The tool_search boundary accepts the exact query glass_static_state, without the server name. Call tool_search with query exactly glass_static_state and limit 5, then load and call the returned tool. Continue the authorized static analysis of Strings Challenge 3, record evidence-backed findings, and call glass_static_report when complete. Do not execute or debug the sample."That's all she needed to bring it home. Harnestia made it through the boundary and completed the static analysis:

Let's enter the flag into the challenge answer box:

All three correct on the first attempt. Wow.