PII-Guard for KI-Agents

I am very concerned that when using AI agents, sensitive data does not leave my on-premises environment (particularly not to LLMs), yet it is still possible to set up automations within my environment that use common data such as credentials, API keys, bearer tokens and the like, for example in scripts or CLI commands.

To this end, I have developed a runbook/prompt and would appreciate your feedback and suggestions for optimisation.

@Stll0 @oneitonitram

runbook

Operations Runbook: AI Systems Data Privacy, Secret Injection, PII-Guard, and Audit

Classification: STRICT CONFIDENTIAL / ON-PREMISE ONLY
Target Systems: Hermes, AgentZero, OpenClaw
Environment: Local NethServer 8, internal macOS and Linux systems

Purpose

This runbook defines the operational execution for data privacy, secret injection, PII-Guard hardening, system-specific implementation, and auditing. It serves as an executable working document for AI systems to prepare, implement, verify, and document changes.

Scope

This runbook applies to:

  • The Hermes agent and associated plugins, specifically pii_guard.
  • AgentZero workflows and local automations.
  • OpenClaw instances and comparable internal agent systems.
  • Local scripts, shell commands, Python automations, SSH-based processes, and configuration files.

Operating Principles

  • Sensitive data may be used functionally on a local level but must not be unnecessarily exposed externally.
  • Secrets must never be hardcoded; they must only be provided via secret injection.
  • Whitelist domains and private IPv4 addresses remain functionally operational internally.
  • Explicit approval and auditing are mandatory if sensitive data needs to be exposed externally.
  • Semantic data privacy complements technical Regex detection.

Security Header

Place this header at the beginning of relevant system prompts:

[CRITICAL SECURITY & DATA PRIVACY POLICY]
You operate in a closed on-premise environment. Your absolute priority is the protection of confidential data (GDPR Art. 6/9 PII, health data, passwords, API keys, private keys).

  1. Never leak PII and real secrets in plain text to users, external LLMs, or third-party services. Use placeholders.
  2. Never hardcode secrets in generated scripts or CLI commands. Mandatory use of environment variables is required.
  3. FQDNs and internal IPs for domain1.de, domain2.de, tertilts.de, domain4.de, as well as private IPv4 networks, may be functionally used unmasked internally, but must never be sent externally combined with secrets in an uncontrolled manner.
  4. If a task requires the external transmission of sensitive data, pause, state the rule conflict, obtain approval, and use an auditable approval mechanism.

Data Classes

Protection Classes

Class Examples Handling
Classic PII Email, phone, address, DOB, IBAN Mask or minimize
Art. 9 Data Health data, political opinions, religion Strictly protect, semantic anonymization
IT Secrets API key, Bearer token, JWT, password, private key Never expose externally in plain text
Infrastructure FQDN, hostname, internal IP Internally allowed, minimize externally

System Roles

Hermes

  • Technically expand the PII-Guard.
  • Integrate whitelist logic into __init__.py.
  • Enforce the audit and approval flow for edge cases.

AgentZero

  • Implement secret injection in shell and agent workflows.
  • Establish .env-based runtime usage.
  • Ensure no secrets are hardcoded in prompt or code templates.

OpenClaw

  • Apply the same data privacy model.
  • Use semantic detection of sensitive content as a supplement.
  • Keep internal infrastructure usable while preventing external leakage.

Runbook Execution Flow

Phase 1: Preparation

Goal

Before any change, the system must determine which component is being altered, which sensitive data classes are affected, and whether the change will take effect locally or externally.

Tasks

  1. Identify the target system.
  2. Determine the affected files, modules, and runtime paths.
  3. Check if secrets, whitelist domains, or private IPs are involved.
  4. Determine whether an approval or audit requirement arises.

Guiding Questions

  • Is the work strictly local?
  • Are external APIs or external LLMs touched?
  • Does the task contain credentials, health data, or other highly sensitive content?
  • Must Regex detection, semantic fallback, or both be applied?

Phase 2: PII-Guard Implementation

Goal

The Hermes PII-Guard is expanded so that new secret types are detected and internally permitted infrastructure values are not unnecessarily redacted.

Target File

~/.hermes/plugins/pii_guard/__init__.py

Implementation Task

Add the following logic:

ALLOWED_DOMAINS = (
    "domain1.de",
    "domain2.de",
    "tertilts.de",
    "domain4.de",
)

def is_private_ipv4(value: str) -> bool:
    return bool(re.match(
        r'^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)',
        value
    ))

def is_whitelisted(value: str, type_id: str) -> bool:
    val = value.lower().strip()

    if type_id == "ipv4":
        return is_private_ipv4(val)

    if type_id in ("fqdn", "email", "ssh_user_host"):
        return any(
            val == domain
            or val.endswith("." + domain)
            or val.endswith("@" + domain)
            for domain in ALLOWED_DOMAINS
        )

    return False

Regex Expansion

Place these patterns at the beginning of PII_PATTERNS:

("ssh_private_key_header", "SSH Private Key", re.compile(
    r'-----BEGIN (?:RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY-----',
    re.IGNORECASE
)),
("jwt_token", "Bearer/JWT Token", re.compile(
    r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b'
)),
("bearer_token", "Bearer Token", re.compile(
    r'\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b',
    re.IGNORECASE
)),
("api_key_generic", "API Key", re.compile(
    r'\b(?:api[_-]?key|apikey|secret|client[_-]?secret|access[_-]?token|refresh[_-]?token)["\']?\s*[:=]\s*["\']?[A-Za-z0-9\-._~+/=]{12,}["\']?',
    re.IGNORECASE
)),
("password_assignment", "Passwort/Zugangsdaten", re.compile(
    r'\b(?:password|passwd|pwd|passwort)["\']?\s*[:=]\s*["\']?.{6,}["\']?',
    re.IGNORECASE
)),
("ssh_user_host", "SSH Login", re.compile(
    r'\b[a-z_][a-z0-9._-]{0,31}@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b',
    re.IGNORECASE
)),
("fqdn", "FQDN/Hostname", re.compile(
    r'\b(?=.{4,253}\b)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+(?:[A-Za-z]{2,63})\b'
)),
("ipv4", "IPv4-Adresse", re.compile(
    r'\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b'
)),

Function Adjustments

In scan_for_pii():

matched = match.group(0)
if is_whitelisted(matched, type_id):
    continue

In redact_pii():

if is_whitelisted(matched, type_id):
    return matched

Implementation Rules

  • Secret patterns must be placed before general infrastructure patterns.
  • Whitelist exceptions apply internally only.
  • Real secrets must never needlessly enter model contexts, despite internal use.
  • Regex detection does not replace semantic evaluation.

Phase 3: Setting up Secret Injection

Goal

Secrets are stored in a local, restrictively protected structure and injected at runtime.

Target Structure

Preferred paths:

  • ~/.hermes/.secrets/
  • /a0/.secrets/

Mandatory Steps

  1. Create directory .secrets/.
  2. Set permissions to 700.
  3. Use a global.env file with permissions set to 600.
  4. Only allow runtime inclusion via source or os.getenv().

Script Task: add_secret.sh

Requirements:

  • Accepts a Key and Value.
  • Creates global.env if it doesn’t exist.
  • Updates existing keys idempotently.
  • Never outputs secret values into logs or standard output.

Script Task: audit_secrets.sh

Requirements:

  • Enforces chmod 600 global.env.
  • Lists only the existing keys.
  • Never outputs secret values.

Example output:

API_TOKEN
DB_PASSWORD
SSH_DEPLOY_KEY

Runtime Rule

Shell commands involving secrets must load the env file beforehand:

source /path/to/.secrets/global.env

Python code must utilize environment variables:

import os
api_token = os.getenv("API_TOKEN")

Phase 4: Semantic Data Privacy Fallback

Goal

Even if Regex patterns don’t trigger, the system must recognize when sensitive information is present based on the context.

Mandatory Behavior

  • Never replicate Art. 9 data in plain text.
  • Treat unstructured credentials as sensitive.
  • Anonymize free text containing diagnoses, political statements, or other highly sensitive details.
  • Use placeholders, such as [DIAGNOSE_REDACTED] or [SECRET_REDACTED].

Example

Not allowed:

  • Full reproduction of a diagnosis with identifiable details.
  • Repeating an API secret posted by the user in the response.

Allowed:

  • Describing the function.
  • Abstracted, anonymized, or masked representation.

Phase 5: Testing and Verification

Mandatory Tests for PII-Guard

Test Case Expectation
admin@domain1.de Remains unredacted internally
ssh root@host.domain1.de Remains unredacted internally
hermes.home.domain1.de Remains unredacted internally
192.168.3.21 Remains unredacted internally
Bearer eyJ... Is redacted
password=SuperSecret123 Is redacted
-----BEGIN OPENSSH PRIVATE KEY----- Is redacted

Mandatory Tests for Secret Injection

  • global.env exists.
  • Permissions are correct.
  • audit_secrets.sh shows only keys.
  • Scripts function without hardcoding.
  • Logs contain no plain-text secrets.

Mandatory Tests for Semantic Fallback

  • Health-related free texts are anonymized.
  • Political or religious details are not unnecessarily repeated.
  • Unstructured secrets are masked rather than paraphrased.

Phase 6: Audit

Goal

A traceable audit is conducted after every change.

Audit Checklist

  • Was the correct file modified?
  • Was a backup or patch documentation created?
  • Are Regex patterns sorted in a logical order?
  • Do whitelist domains and private IPs remain functional internally?
  • Are secrets reliably detected and redacted?
  • Was secret injection implemented instead of hardcoding?
  • Are permissions on .secrets and global.env correct?
  • Were only keys logged, not values?
  • Does the semantic data privacy apply in non-regular cases?
  • Is there any external leakage of sensitive data?

Audit Log Template

## Audit Log

- Date/Time:
- Target System:
- Affected Component:
- Modified File(s):
- Goal of Change:
- Tests Performed:
- Test Results:
- Open Risks:
- Approval Status:
- Next Steps:

Phase 7: Approval and Escalation Logic

Rule

If a task requires the external transmission of sensitive data, the system must pause and obtain explicit approval.

Mandatory Process

  1. Identify the conflict.
  2. State the risk.
  3. Identify the affected data class.
  4. Obtain user approval.
  5. Use an auditable approval mechanism.
  6. Execute only the approved individual case.

Standard Phrasing

This step would transmit sensitive data, credentials, or protected content to an external system. Explicit approval is required for this single action. The process must be audited.

Phase 8: Rollback

Goal

If tests fail or the change produces unwanted side effects, a safe rollback must be possible.

Minimum Requirements

  • Backup the original file before changes.
  • Cleanly isolate modified Regex and whitelist blocks.
  • Document rollback steps.
  • Perform a smoke test again after rollback.

Rollback Check

  • Original state restored.
  • Plugin loads without errors.
  • No unintended exposure of sensitive data.
  • Internal workflows remain operational.

Phase 9: Sign-off

Approval Criteria

The change is only considered accepted if all the following points are met:

  • PII-Guard detects the new secret types.
  • Whitelist domains and private IPs remain functional internally.
  • Secret injection is configured and usable.
  • Hardcoding has been avoided.
  • Audit was fully completed.
  • There is no uncontrolled external leakage of sensitive data.

Appendix A: Example Work Instruction to an AI System

Analyze the target system and strictly apply this runbook. 
1. Identify affected components. 
2. Implement necessary changes system-specifically. 
3. Use secret injection instead of hardcoding. 
4. Patch the PII-Guard according to the defined Regex and Whitelist rules. 
5. Perform tests and audits. 
6. Document results, risks, and approval status. 
7. Mandatory: ask for approval if external exposure of sensitive data is involved.

Appendix B: Impermissible Patterns

Specifically not allowed:

  • Secrets in the prompt in plain text.
  • Secrets in generated code in plain text.
  • External transmission of health data or credentials without approval.
  • Bypassing redacted tool outputs.
  • Logging of secret values during the audit.

Appendix C: Permissible Patterns

Specifically allowed:

  • Local usage of secrets via env variables.
  • Internal usage of whitelist FQDNs and private IPs.
  • Redacted or anonymized representation of sensitive content.
  • Audited approval processes for justified individual cases.

thank you for this, ill try to implement it, with some scenarios i have in mind, and see, the level of hardening it provides. then ill report my findings, as well as improvements necessary.

1 Like

Now a related problem has arisen: A0 no longer processes credentials in plain text but masks them immediately.

AgentZero apparently treats the values you enter as chat/browser content rather than as genuine runtime secrets. As a result, they are masked, do not end up as internally resolvable variables in the execution context, and cannot be used during login.

The error message shows exactly this pattern: the agent expects downstream secret resolution, but only receives the literal strings §§ secret (USER) and §§secret (PASSWORD).

This is why every login fails, even though the placeholder looks logically correct.

possible solution?

The credentials must be stored outside the prompt/browser context and only injected locally during the actual login step. This is appropriate for the setup because it is explicitly intended that local on-premises automation works with secrets without them entering LLM-facing contexts.

A way must therefore be found to provide and manage credentials.

Clean target architecture:

• Store secrets at host or container level as environment variables or in a strictly protected .env/secret file.

• AgentZero is only given aliases such as USER and PASSWORD.

• A local resolver replaces §§secret (USER) immediately before page.fill(…), type(…) or the browser login step.

• The plain text is never rewritten into the chat, browser prompt or project text.

Secrets are stored in the container, for example:
LOGIN_USER=‘youruser’
LOGIN_PASSWORD=‘yourpassword’
AgentZero must be able to read them at runtime, e.g. via:
• container environment variables
• a mounted .env file
• a local secret loader

In the browser workflow, you cannot use plain text for chat messages; instead, you must use a resolver function such as:

import os
import re

def resolve_secret(value: str) -> str:
    def repl(match):
        key = match.group(1)
        secret = os.getenv(key)
        if not secret:
            raise ValueError(f"Secret {key} nicht gesetzt")
        return secret
    return re.sub(r"§§secret\\\(([^)]+)\\\\)", repl, value)

during login:

username = resolve_secret("§§secret(LOGIN_USER)")
password = resolve_secret("§§secret(LOGIN_PASSWORD)")

page.fill('input[name=\"username\"]', username)
page.fill('input[name=\"password\"]', password)
page.click('button[type=\"submit\"]')

Important: The resolution must take place in the tool/runtime code, not in the LLM prompt.

A0 now requires an instruction such as:

If a value is passed as §§secret(NAME), you must never ask the user for the plaintext in the chat.
If the alias cannot be resolved, abort the step and report:
“Secret alias not available in the local runtime store.”
Expect secrets exclusively from local environment variables, secret files with restrictive permissions, or a dedicated secret resolver.
Never permanently store login credentials from the chat in plain text.

The most sensible approach is a multi-level credential registry: separate storage for each target system, plus a central index, so that AgentZero can automatically find the appropriate set of credentials at runtime based on the system name, hostname or URL. This aligns with your desire to keep credentials locally usable in production whilst also keeping them securely contained within container/runtime variables with audit trails.
Structure
I would not store the credentials as a single global.env file, but would separate them into three levels:

  1. Registry: Assigns systems, domains and aliases to a credential profile.
  2. Credential sets: A separate secret file for each system or account.
  3. Resolver: Searches for the correct profile when needed and loads only that one.
    Recommended folder structure:
/a0/.secrets/
├── registry/
│   └── systems.yaml
├── systems/
│   ├── hermes-dashboard/
│   │   └── default.env
│   ├── agentzero-dashboard/
│   │   └── default.env
│   ├── nextcloud/
│   │   ├── admin.env
│   │   └── readonly.env
│   ├── wordpress-main/
│   │   └── editor.env
│   └── ns8/
│       ├── root.env
│       └── operator.env
└── runtime/
    └── active_session.env

At its core is a small registry file that says: “When target X is requested, use profile Y.”

Beispiel systems.yaml:

systems:
  - id: hermes-dashboard
    match:
      aliases: ["hermes", "hermes-agent", "hermes dashboard"]
      hosts: ["hermes.sub.mydomain.de"]
      urls: ["https://hermes.sub.mydomain.de]
    credential_profile: "default"
    secret_file: "/a0/.secrets/systems/hermes-dashboard/default.env"

  - id: agentzero-dashboard
    match:
      aliases: ["agentzero", "agent zero", "agentzero dashboard"]
      hosts: ["agentzeroai.sub.mydomain.de"]
      urls: ["https://agentzeroai.sub.mydomain.de"]
    credential_profile: "default"
    secret_file: "/a0/.secrets/systems/agentzero-dashboard/default.env"

  - id: ns8
    match:
      aliases: ["ns8", "nethserver", "nethserver 8"]
      hosts: ["192.168.3.21", "daho-ns.sub.mydomain.de"]
    credential_profile: "root"
    secret_file: "/a0/.secrets/systems/ns8/root.env"

  - id: wordpress-main
    match:
      aliases: ["wordpress", "blog"]
      hosts: ["blog.example.tld"]
      urls: ["https://blog.example.tld/wp-login.php"]
    credential_profile: "editor"
    secret_file: "/a0/.secrets/systems/wordpress-main/editor.env"

The architecture must also take into account that multiple logins are possible per system.

The Secret files should not only contain LOGIN_USER and LOGIN_PASSWORD , but also standardised fields based on the authentication type. Otherwise, the system will quickly become confusing (others like ssh key, bearer token …)

The most practical approach is not manual editing via SSH, but a local secret onboarding process that accepts credentials in a structured way and writes them directly to your secret store. This suits your setup because you want to use credentials locally in production, but don’t want the hassle of repeated maintenance via chat or cumbersome SSH editors.
Best option
The best solution is a small local admin frontend for secrets, accessible only on your home network or via a login, which writes new systems, accounts and secret types to your registry and secret files. This keeps the data outside the LLM context, and AgentZero can later find it specifically per system and per account.
The frontend should be able to do three things: select or create a new system, select an account/role, and enter the authentication type, such as password, bearer token or SSH key. It then automatically writes to /a0/.secrets/… , sets permissions correctly and updates the registry without you needing nano or vi . [Memory]
Useful input methods
I would prioritise these four methods. They vary in convenience, but all are better than constantly editing files manually via SSH.
• Local web interface: Form for system ID, account ID, authentication type and secret values; ideal for day-to-day use.
• Import file: Upload a prepared YAML/JSON/CSV file containing multiple credentials at once; good for initial population.
• One-off command without an editor: A small add-secret script with parameters or interactive prompts; good as a fallback if you are already logged in via SSH. [Memory]
• Watch folder import: You place a file locally in a monitored folder, and an importer moves it to the secret store; handy for Mac-to-server workflows in your mixed setup.

As I’m not a developer, the only option available to me at the moment is to provide a YAML file for import.

As someone who isn’t a programmer, the only option available to me at the moment is to provide a YAML file for import via infarge.
The import file should not be used directly by AgentZero as a runtime source, but only as an onboarding format. A local importer reads it once, writes the actual secret files to /a0/.secrets/… , sets permissions correctly and creates or updates the central registry.
This allows you to clearly separate:
• Import file for collection and bulk maintenance,
• Secret store for runtime use,
• Registry for system-related mapping by AgentZero.
Format
I would choose the following structure:
• global metadata,
• list of systems,
• multiple accounts per system,
• one auth_type per account,
• appropriate fields depending on auth_type ,
• optional tags such as purpose , priority , default .
Example:

version: 1
created_by: "manual-import"
description: "Credential-Onboarding für AgentZero"

systems:
  - system_id: hermes-dashboard
    display_name: "Hermes Dashboard"
    match:
      aliases: ["hermes", "hermes dashboard"]
      hosts: ["hermes.sub.mydomain.de"]
      urls: ["https://hermes.sub.mydomain.de"]
    accounts:
      - account_id: admin
        default: true
        priority: 100
        purpose: "web_login"
        auth_type: password_form
        secrets:
          LOGIN_USER: "mein-user"
          LOGIN_PASSWORD: "my-password"
          LOGIN_URL: "https://hermes.sub.mydomain.de"
          USERNAME_SELECTOR: "input[name='username']"
          PASSWORD_SELECTOR: "input[name='password']"
          SUBMIT_SELECTOR: "button[type='submit']"

  - system_id: ns8
    display_name: "NethServer 8"
    match:
      aliases: ["ns8", "nethserver", "nethserver 8"]
      hosts: ["192.168.3.21", "daho-ns.sub.mydomain.de"]
    accounts:
      - account_id: root-key
        default: true
        priority: 100
        purpose: "ssh_admin"
        auth_type: ssh_key
        secrets:
          SSH_USER: "root"
          SSH_HOST: "192.168.3.21"
          SSH_PORT: "22"
          SSH_KEY_EMBEDDED: |
            -----BEGIN OPENSSH PRIVATE KEY-----
            ...
            -----END OPENSSH PRIVATE KEY-----
          SSH_PASSPHRASE: ""

      - account_id: ui-admin
        priority: 80
        purpose: "web_login"
        auth_type: password_form
        secrets:
          LOGIN_USER: "admin"
          LOGIN_PASSWORD: "other-password"
          LOGIN_URL: "https://daho-ns.sub.mydomain.de"

  - system_id: internal-api
    display_name: "Internal API"
    match:
      aliases: ["internal api", "api"]
      hosts: ["api.sub.mydomain.de"]
      urls: ["https://api.sub.mydomain.de"]
    accounts:
      - account_id: default
        default: true
        priority: 100
        purpose: "api"
        auth_type: bearer_token
        secrets:
          API_BASE_URL: "https://api.sub.mydomain.de"
          AUTH_HEADER: "Authorization"
          AUTH_SCHEME: "Bearer"
          BEARER_TOKEN: "my-token"

Processing
The importer should generate three items from this file:

  1. A registry file, for example registry/systems.yaml , containing match rules and account metadata.
  2. Secret files, for example:
    /a0/.secrets/systems/hermes-dashboard/admin.env
    /a0/.secrets/systems/internal-api/default.env
  3. SSH key files, if SSH_KEY_EMBEDDED is present, for example:
    /a0/.secrets/keys/ns8__root-key

For SSH keys, the importer should not leave the embedded key from the YAML in the .env file, but should write it to a separate key file and only store SSH_KEY_FILE=/a0/.secrets/keys/ns8__root-key in the corresponding .env file. This is cleaner and more secure for SSH tools.

Rules for the importer
The importer should perform strict validation:
• system_id must be unique.
• account_id must be unique within a system.
auth_type must correspond to an allowed list, such as password_form, password_basic, bearer_token, ssh_key.
• For each account, the required fields must be present for each auth type.
• It must never be overwritten implicitly without permission.
• After the import, permissions must be set and an audit generated.

To cut a long story short:
Both A0 and Hermes are in urgent need of an architecture to protect sensitive data.

It would be a great contribution from professional developers such as @oneitonitram or @Stll0 to equip the apps offered here accordingly, even if it means using workarounds.

I’m certainly not the only AI user who is concerned about privacy, and a solution like this would significantly improve acceptance.

@capote note that agentzero does have an implementation built in for secrets management.
which you can use to store credentials, and only reference the secret, which will prefill, whatever secrets are needed.

this is part of security hardening they have been doing recently, and they have been increasing a bounty every week for cracking any credentials stored or shared within agentzero.

go to settings,


on the left panel, we have secrets.

store all secrets over there, and can be used accross the entire agentzero workspace, including in their browser, and desktop view.

1 Like

Thanks for your advice; I completely overlooked that.

1 Like