> For the complete documentation index, see [llms.txt](https://docs.apono.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apono.io/docs/agent-privilege-guard/set-up-apono-agent-privilege-guard/apono-agent-privilege-guard-for-aws/restrict-ai-agents-to-apono-agent-privilege-guard-for-aws.md).

# Restrict AI agents to Apono Agent Privilege Guard for AWS

Make the Apono Agentic Gateway the only way AI agents can access AWS

Configure Claude Code, Claude Desktop, and Codex so that the Apono Agentic Gateway is the only way an AI agent can access AWS.

After Apono Agent Privilege Guard is set up for AWS, the AI agent has two ways to reach AWS: the Agentic Gateway, and the AWS credentials already on the developer's machine under `~/.aws`. This page shows how to close the second path.

Each AI client gets three kinds of settings:

* **Deny rules** stop the agent from running the AWS CLI and from reading `~/.aws`.
* **Instructions** tell the agent that the block is intentional and that AWS access goes through the Apono MCP tools `list_targets` and `ask_access_assistant`.
* **An OS-level deny** makes `~/.aws` unreadable to every process the agent starts. This is the only layer that is a real security boundary.

***

### How the layers differ

<table><thead><tr><th width="180">Layer</th><th>What it does</th><th width="200">Enforced by</th></tr></thead><tbody><tr><td><strong>Instructions</strong></td><td>Tells the agent that AWS goes through Apono. The agent stops trying and points the user to Apono.</td><td>The model. Guidance only.</td></tr><tr><td><strong>Client rules</strong></td><td>The client refuses to run a matching command or read a matching path. Gives a clear refusal and an audit trail.</td><td>The client. Can be bypassed.</td></tr><tr><td><strong>OS-level deny</strong></td><td>The operating system blocks every process the agent starts from reading <code>~/.aws</code>, including scripts.</td><td>The operating system. The security boundary.</td></tr></tbody></table>

{% hint style="info" %}
**Block the credential, not the command.** There are endless ways to run a command. There is only one `~/.aws` folder. Use command rules for a clear refusal. Use the credential-path deny as the control that holds.
{% endhint %}

***

### Prerequisites

<table><thead><tr><th width="220">Requirement</th><th>Details</th></tr></thead><tbody><tr><td><strong>Apono account setup completed</strong></td><td><p>Agent Privilege Guard for AWS is set up</p><p>Confirm that an Apono admin has followed <a href="/docs/agent-privilege-guard/set-up-apono-agent-privilege-guard/apono-agent-privilege-guard-for-aws.md">Apono Agent Privilege Guard for AWS</a>: the AWS Organization integration is connected, an agentic access flow exists with <strong>Restrict to agentic access</strong> enabled, and MCP features and the AWS MCP are enabled.</p></td></tr><tr><td><strong>Agentic Gateway connected</strong></td><td><p>The AI client already reaches Apono</p><p>Each AI client you restrict must already be connected through the <a href="/docs/agent-privilege-guard/apono-agentic-gateway/install-the-local-agentic-gateway.md">Local Agentic Gateway</a>, and asking it to list available integrations must return AWS. Apply the deny rules only after this works. Otherwise the developer loses AWS access with no alternative.</p></td></tr><tr><td><strong>Admin route to each AI client</strong></td><td><p>How the settings reach every machine</p><ul><li><strong>Claude Code:</strong> the claude.ai admin console, or a <code>managed-settings.json</code> file deployed by MDM.</li><li><strong>Claude Desktop:</strong> managed preferences deployed by MDM.</li><li><strong>Codex:</strong> a root-owned <code>requirements.toml</code> deployed by MDM, or ChatGPT managed configs with a Business or Enterprise sign-in.</li></ul></td></tr></tbody></table>

***

### Configure your AI client

{% tabs %}
{% tab title="Claude Code" %}
Claude Code's managed settings cover the CLI, the VS Code and JetBrains extensions, and Claude Desktop's Code and Cowork surfaces.

Managed settings outrank every other setting level. No user, project, or command-line setting can override a managed deny rule. A managed settings file applies to every Claude Code session on the machine, whichever account the developer signs in with.

**Deliver the configuration**

Deploy it as server-managed settings in the claude.ai admin console, or as a file on each machine:

<table><thead><tr><th width="160">Platform</th><th>Managed settings path</th></tr></thead><tbody><tr><td>macOS</td><td><code>/Library/Application Support/ClaudeCode/managed-settings.json</code></td></tr><tr><td>Linux / WSL</td><td><code>/etc/claude-code/managed-settings.json</code></td></tr><tr><td>Windows</td><td><code>C:\Program Files\ClaudeCode\managed-settings.json</code></td></tr></tbody></table>

**Configuration**

{% code overflow="wrap" expandable="true" %}

```json
{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "allowManagedPermissionRulesOnly": true,
  "claudeMd": "AWS access on this machine goes through Apono, not through local credentials.\n\nThe files under ~/.aws and the AWS_* credential environment variables are blocked by organization policy. Commands such as `aws`, `terraform`, and boto3 scripts will fail to find credentials. This is intentional and is not a misconfiguration to work around.\n\nWhen you need AWS access, use the Apono MCP tools: call `list_targets` to see what is already reachable, or `ask_access_assistant` to request access. Do not ask the user to widen permission rules, unset the deny list, or hand you credentials.",
  "permissions": {
    "allow": [
      "Bash"
    ],
    "deny": [
      "Bash(aws *)",
      "Bash(/usr/local/bin/aws *)",
      "Bash(/opt/homebrew/bin/aws *)",
      "Bash(aws-vault *)",
      "Read(//Users/*/.aws/**)",
      "Edit(//Users/*/.claude/settings.json)"
    ],
    "disableBypassPermissionsMode": "disable"
  }
}
```

{% endcode %}

<table><thead><tr><th width="280">Setting</th><th>Purpose</th></tr></thead><tbody><tr><td><strong>permissions.deny</strong></td><td>Blocks the <code>aws</code> command by name and at its two common install paths, blocks <code>aws-vault</code>, and blocks reads under <code>~/.aws</code> for every user on the machine. Path rules also cover shell file commands such as <code>cat</code>, <code>head</code>, <code>tail</code>, and <code>sed</code>.</td></tr><tr><td><strong>claudeMd</strong></td><td>Instructions added to every session: the block is intentional, and AWS goes through the Apono MCP tools. Works only in managed settings.</td></tr><tr><td><strong>allowManagedPermissionRulesOnly</strong></td><td>Makes managed settings the only source of permission rules.</td></tr><tr><td><strong>disableBypassPermissionsMode</strong></td><td>Removes bypass-permissions mode.</td></tr></tbody></table>

{% hint style="info" %}
To test a configuration before deploying it, run Claude Code with `claude -p --settings <file>`.
{% endhint %}

**Add the OS-level deny**

The deny rules do not stop a script that opens `~/.aws/credentials` on its own, such as a Python one-liner. Without the sandbox, only the auto-mode classifier stands in the way, and a classifier is not a guarantee.

Follow these steps:

1. Keep auto mode on. Do not set `permissions.disableAutoMode`.
2. In the same managed settings, enable the sandbox and list `~/.aws` under `sandbox.credentials`. The sandbox blocks reads at the OS level for every command the agent runs and every child process. See [Sandboxing](https://code.claude.com/docs/en/sandboxing).

{% hint style="warning" %}
The sandbox runs on macOS, Linux, and WSL2. It does not run on native Windows.
{% endhint %}
{% endtab %}

{% tab title="Claude Desktop" %}
Claude Code's managed settings already cover Claude Desktop's Code and Cowork surfaces. This configuration adds Desktop's own deny for the AWS command and `~/.aws`, and turns off desktop extensions.

Deliver it as managed preferences. When a managed source is present, it wins and local values are ignored.

<table><thead><tr><th width="160">Platform</th><th>Managed location</th></tr></thead><tbody><tr><td>macOS</td><td><code>/Library/Managed Preferences/&#x3C;user>/com.anthropic.claudefordesktop.plist</code></td></tr><tr><td>Windows</td><td><code>HKLM\SOFTWARE\Policies\Claude</code> (machine) / <code>HKCU\SOFTWARE\Policies\Claude</code> (user)</td></tr><tr><td>Linux</td><td><code>/etc/claude-desktop/managed-settings.json</code></td></tr></tbody></table>

Values, JSON encoded as a string in the plist or registry:

{% code overflow="wrap" %}

```json
{
  "disabledBuiltinTools": [
    "Bash(aws *)",
    "Read(**/.aws/**)",
    "Edit(**/.aws/**)"
  ],
  "isDesktopExtensionEnabled": false,
  "isDesktopExtensionSignatureRequired": true
}
```

{% endcode %}

macOS plist equivalent:

{% code overflow="wrap" %}

```xml
<key>disabledBuiltinTools</key>
<string>["Bash(aws *)","Read(**/.aws/**)","Edit(**/.aws/**)"]</string>
```

{% endcode %}

Each `disabledBuiltinTools` entry is a tool name such as `Bash`, or a scoped rule such as `Bash(aws *)`. A bare name blocks every call. A scoped rule blocks matching calls in every permission mode.

{% hint style="warning" %}
**Caveats**

1. Cowork's own shell honors bare tool names only. `Bash(aws *)` has no effect there. A bare `"Bash"` entry works.
2. Start file patterns with `**/`. An absolute host path does not match inside the Cowork sandbox.
3. A local MCP server that runs `aws` is an `mcp__*` tool, not `Bash`, so these rules do not apply to it. Use `managedMcpServers[].toolPolicy` to block or gate its tools.
4. Device Claude Code policy overrides Desktop policy unless the Claude Code managed settings set `parentSettingsBehavior: "merge"`.
5. Scoped rules depend on the Desktop build. Pin builds with `disableAutoUpdates`.
   {% endhint %}
   {% endtab %}

{% tab title="Codex" %}
Codex has the strongest credential-path control of the three clients and a weak command control.

Admin-enforced settings live in `requirements.toml`. Users cannot weaken them with local config. Codex reads them, from lowest to highest priority, from `/etc/codex/requirements.toml` (macOS and Linux) or `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows), then the cloud config assigned at `chatgpt.com/codex/settings/managed-configs`, then the macOS MDM key `com.openai.codex:requirements_toml_base64`.

{% hint style="warning" %}
`/etc/codex/config.toml` ranks **below** user config. `managed_config.toml` sets defaults the user can change during a run. Only `requirements.toml` is a lock.
{% endhint %}

Follow these steps:

1. Install the following file root-owned at `/etc/codex/requirements.toml`, or deliver it through the MDM key or cloud managed configs.

{% code overflow="wrap" expandable="true" %}

```toml
# Codex ADMIN-ENFORCED layer. Users cannot weaken these.
#   Unix (incl. macOS): /etc/codex/requirements.toml
#   Windows:            %ProgramData%\OpenAI\Codex\requirements.toml
#   macOS MDM:          com.openai.codex -> requirements_toml_base64
# Cloud requirements (chatgpt.com/codex/settings/managed-configs) outrank this file.
# NOTE: top-level scalars MUST come before any [table] header or they are absorbed into it.

allowed_approval_policies = ["untrusted", "on-request"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
default_permissions = "corp_no_cloud_creds"
allow_managed_hooks_only = true

[allowed_permission_profiles]
":read-only" = true
corp_no_cloud_creds = true          # ":danger-full-access" omitted => denied

[permissions.corp_no_cloud_creds]
description = "Workspace access with local cloud credentials denied."
extends = ":workspace"

[permissions.corp_no_cloud_creds.filesystem]
"~/.aws" = "deny"

# "filesystem" is a reserved profile name here. THE strong control:
# admin-enforced read denial, users cannot weaken it with local config.
[permissions.filesystem]
deny_read = ["~/.aws"]

# Defense in depth: the AWS CLI is useless without reachable endpoints.
[experimental_network]
enabled = true
denied_domains = ["*.amazonaws.com"]

# Per-command deny. EXPERIMENTAL, and scoped to the OUTSIDE-sandbox boundary only.
[rules]
prefix_rules = [
  { pattern = [{ token = "aws" }], decision = "forbidden", justification = "AWS access goes through Apono, not local credentials." },
  { pattern = [{ token = "terraform" }], decision = "forbidden", justification = "AWS access goes through Apono, not local credentials." },
]

# The only UNCONDITIONAL per-command block. Admin must ship the script separately;
# requirements.toml enforces the hook config but does not distribute managed_dir contents.
[features]
hooks = true

[hooks]
managed_dir = "/etc/codex/hooks"

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 /etc/codex/hooks/block_aws.py"
timeout = 10
statusMessage = "Checking command against AWS credential policy"
```

{% endcode %}

<table><thead><tr><th width="280">Setting</th><th>Purpose</th></tr></thead><tbody><tr><td><strong>permissions.filesystem.deny_read</strong></td><td>The OS-level deny. Codex keeps the agent in a sandbox so it can enforce it. Users cannot weaken it.</td></tr><tr><td><strong>experimental_network.denied_domains</strong></td><td>Blocks AWS endpoints, so the AWS CLI is useless even if it runs.</td></tr><tr><td><strong>rules.prefix_rules</strong></td><td>Refuses <code>aws</code> and <code>terraform</code> with a reason the agent can read. Applies only outside the sandbox, so treat it as a clear message, not a block.</td></tr><tr><td><strong>hooks.PreToolUse</strong></td><td>Runs the script below before every shell command. This is the unconditional command block, and its deny reason is the instruction that points the agent to Apono.</td></tr></tbody></table>

2. Install the hook script root-owned at `/etc/codex/hooks/block_aws.py`. Codex enforces the hook configuration but does not distribute the script, so ship it in the same MDM payload.

{% code overflow="wrap" expandable="true" %}

```python
#!/usr/bin/env python3
"""Codex PreToolUse hook: block local AWS credential use.

Install root-owned at /etc/codex/hooks/block_aws.py, referenced from
requirements.toml. Codex enforces the hook configuration but does NOT
distribute the script, so ship it with the same MDM payload.

stdin:  one JSON object. For Bash the command is at tool_input.command.
stdout: a deny MUST use the nested hook-specific shape. A bare
        {"permissionDecision": "deny"} fails schema validation, and an
        invalid hook result fails OPEN, so the command would run.
        Allow = exit 0 with no output. Exit 2 + stderr is the fallback.
"""
import json
import os
import re
import shlex
import sys

BLOCKED_PROGRAMS = {"aws", "aws-vault", "terraform"}
WRAPPERS = {
    "env", "timeout", "time", "nice", "nohup", "stdbuf", "command",
    "builtin", "sudo", "doas", "xargs", "setsid",
}
CREDENTIAL_REFERENCE = re.compile(
    r"\.aws/|/\.aws\b|AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY|SESSION_TOKEN|PROFILE)|boto3"
)
SUBCOMMAND_SEPARATOR = re.compile(r"\|\||&&|[;|&\n]")

DENY_REASON = (
    "Local AWS credentials are blocked by organization policy. "
    "AWS access goes through Apono: use list_targets or ask_access_assistant."
)


def strings_in(value):
    """Every string leaf of a tool-argument payload."""
    if isinstance(value, str):
        return [value]
    if isinstance(value, dict):
        return [leaf for item in value.values() for leaf in strings_in(item)]
    if isinstance(value, (list, tuple)):
        return [leaf for item in value for leaf in strings_in(item)]
    return []


def blocks(text):
    """True when text runs a blocked program or touches AWS credentials.

    Matches the program per subcommand, after stripping leading environment
    assignments and common wrappers, comparing on basename so an absolute
    path such as /usr/local/bin/aws is caught too.
    """
    if CREDENTIAL_REFERENCE.search(text):
        return True
    for segment in SUBCOMMAND_SEPARATOR.split(text):
        try:
            tokens = shlex.split(segment)
        except ValueError:
            tokens = segment.split()
        while tokens:
            token = tokens.pop(0)
            if "=" in token.split("/")[0] or token.startswith("-"):
                continue
            program = os.path.basename(token)
            if program in WRAPPERS:
                continue
            if program in BLOCKED_PROGRAMS:
                return True
            break
    return False


def payload_blocks(tool_arguments):
    return any(blocks(leaf) for leaf in strings_in(tool_arguments))


def main():
    try:
        payload = json.load(sys.stdin)
    except (json.JSONDecodeError, ValueError):
        print(DENY_REASON, file=sys.stderr)
        sys.exit(2)

    if payload_blocks(payload.get("tool_input")):
        json.dump({"hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": DENY_REASON,
        }}, sys.stdout)
    sys.exit(0)


if __name__ == "__main__":
    main()
```

{% endcode %}

3. (Optional) Install `/etc/codex/managed_config.toml` to remove `AWS_*` variables from the agent's shell. This is a default the user can change during a run, not a lock.

{% code overflow="wrap" %}

```toml
# Codex MANAGED DEFAULTS. Override user config.toml and CLI --config at startup,
# but the user can still change them during the run. NOT an enforcement surface.
#   Unix: /etc/codex/managed_config.toml     Windows: ~/.codex/managed_config.toml
# (Do not confuse with /etc/codex/config.toml, which ranks BELOW user config.)

approval_policy = "untrusted"
sandbox_mode = "workspace-write"

[sandbox_workspace_write]
network_access = false

[shell_environment_policy]
inherit = "core"
ignore_default_excludes = false

[shell_environment_policy.filters]
"AWS_*" = "exclude"
```

{% endcode %}

{% hint style="warning" %}
**A malformed hook deny lets the command run.** A deny must use the nested `hookSpecificOutput` shape shown in the script. A bare `{"permissionDecision":"deny"}` fails validation, and Codex continues the tool call. Exit code `2` with the reason on stderr is the fallback deny.
{% endhint %}

{% hint style="info" %}
Profile allowlists need Codex 0.138.0 or later. On native Windows, `deny_read` applies to Codex's file tools but not to shell subprocess reads.
{% endhint %}
{% endtab %}
{% endtabs %}

***

### Verify the configuration

Perform the following tests inside each AI client after restarting it. Ask the agent for the literal output or error of each command.

<table><thead><tr><th width="60">#</th><th>Command</th><th width="180">Tests</th><th width="180">Unblocked value</th></tr></thead><tbody><tr><td>1</td><td><code>aws sts get-caller-identity</code></td><td>direct command</td><td>caller identity</td></tr><tr><td>2</td><td><code>sh -c 'head -c 40 ~/.aws/credentials | wc -c'</code></td><td>wrapper evasion</td><td><code>40</code></td></tr><tr><td>3</td><td><code>python3 -c "print(len(open('/Users/&#x3C;user>/.aws/credentials').read()))"</code></td><td>subprocess read</td><td>byte size of the file</td></tr><tr><td>4</td><td><code>env | grep AWS_</code></td><td>environment variables</td><td>variable names and values</td></tr></tbody></table>

Probe 3 is the important one. A deny rule can stop it only when the command line names the path. Only the OS-level deny stops the read itself.

Then confirm the Apono path still works: ask the agent to list available integrations. The AWS target should appear.

#### Confirm the deny is enforced, not just obeyed

An agent often declines to touch credential files on its own. That does not prove the deny works. To test the deny itself, deny a harmless file the agent has no reason to refuse.

Follow these steps:

1. Create two files:

```
~/apono-probe/control.txt   contains  SAFE-CONTENT-1234    (not denied)
~/apono-probe/decoy.txt     contains  DECOY-CONTENT-8842   (added to the deny list)
```

2. Add the decoy path to the credential-path deny of the client under test. For Codex, add it to `deny_read`:

```toml
deny_read = ["~/.aws", "~/apono-probe/decoy.txt"]
```

3. Restart the AI client, then ask the agent to `cat` both files and paste the literal output or error for each.

<table><thead><tr><th width="320">Observed</th><th>Conclusion</th></tr></thead><tbody><tr><td>control readable, decoy fails with an OS error</td><td>The deny is enforced.</td></tr><tr><td>both readable</td><td>The deny is not enforced. Earlier refusals were the agent obeying, not the client blocking.</td></tr><tr><td>agent refuses both</td><td>Inconclusive. Rerun and insist on literal output.</td></tr></tbody></table>

***

### Known limitations

* **A refusal is not a block.** An agent that declines to run a command proves the instructions work, not that the client would stop it. Only an OS-level error proves enforcement.
* **Command rules can be bypassed.** Renamed binaries, quoted names such as `a"w"s`, `bash -c`, `docker exec`, `npx`, and SDKs such as `boto3` all get past a command rule. The credential-path deny is the control that holds.
* **Other MCP servers are not covered.** An MCP server that reads `~/.aws` is not a shell command. Restrict MCP servers separately.
* **Environment variables are only partly covered.** Codex removes `AWS_*` as a default the user can change. Claude Code needs the sandbox to do the same.
* **Device files need MDM.** A developer with local admin rights can edit them. Redeploy on a schedule.
* **Console settings need the org account.** Tenant-side policy does not apply when the developer signs in with a personal account.
* **The agent's API is outside all of this.** A script that calls the model API directly is under no client policy.

***

### Troubleshooting

<details>

<summary><strong>The agent read <code>~/.aws/credentials</code> with a Python script and it succeeded.</strong></summary>

In Claude Code, the deny rules do not cover a script that opens the file itself. Confirm `permissions.disableAutoMode` is not set, and enable the sandbox with `~/.aws` under `sandbox.credentials`.

In Codex, confirm the deny is in `/etc/codex/requirements.toml` under `permissions.filesystem.deny_read`, not in `config.toml` or `managed_config.toml`.

</details>

<details>

<summary><strong>The agent declines every command, but I cannot tell whether the deny is enforced.</strong></summary>

Run the decoy test under [Confirm the deny is enforced, not just obeyed](#confirm-the-deny-is-enforced-not-just-obeyed) and record the literal output.

</details>

<details>

<summary><strong>A Codex hook denies the command but Codex runs it anyway.</strong></summary>

The hook output did not validate. Use the nested `hookSpecificOutput` shape from the script, or exit with code `2` and the reason on stderr.

</details>

<details>

<summary><strong>The Codex policy applies at startup, but the agent changes it mid-session.</strong></summary>

The policy is in `managed_config.toml` or `/etc/codex/config.toml`. Neither is a lock. Move the deny to `/etc/codex/requirements.toml`.

</details>

<details>

<summary><strong>The AWS target no longer appears when the agent lists integrations.</strong></summary>

The deny rules on this page do not touch the Agentic Gateway. Follow [Troubleshooting](/docs/agent-privilege-guard/set-up-apono-agent-privilege-guard/apono-agent-privilege-guard-for-aws.md#troubleshooting) in Apono Agent Privilege Guard for AWS.

</details>

<details>

<summary><strong>Configuration changes are not reflected in the agent.</strong></summary>

Fully restart the AI client.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.apono.io/docs/agent-privilege-guard/set-up-apono-agent-privilege-guard/apono-agent-privilege-guard-for-aws/restrict-ai-agents-to-apono-agent-privilege-guard-for-aws.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
