aws/agent-toolkit-for-aws / Chapter 4

Programming /

E03_The_PreToolUse_Hook

# The PreToolUse Hook > The most important security primitive in the Agent Toolkit for AWS is a 30-line Python script that runs before every `Bash` call. Skip it, and the toolkit is AWS Labs with a new name. ## Key Takeaways - The hook is a **30-line decision function** registered as a `PreToolUse` matcher for `Bash` and any `mcp__aws.*` / `use_aws` call; it returns `permissionDecision: deny` if the call would fetch a secret. - The toolkit's enterprise posture rests on **three primitives**: the **PreToolUse hook** (laptop edge), the **IAM condition keys** (`aws:CalledVia` for AWS edge), and the **secret resolution pattern** (`{{resolve:secretsmanager:...}}` template + `asm-exec`). - Defense in depth: even if the hook fails to block, the secret resolution pattern prevents the value from reaching the agent's context anyway. - The hook is **fail-open on timeout** (`timeout: 5`) — a future hardening opportunity, but a real edge case to know. - The IAM condition keys are the **AWS-side** boundary; the hook is the **laptop-side** boundary. The toolkit ships both, and you need both for a regulated environment. "But Claude Code is a sandbox." I have heard this sentence in three different security review meetings in the last six months. The implication is that the coding agent runs in an isolated environment, that the agent's actions are not the developer's actions, and therefore that the agent cannot leak secrets because the agent has nothing to leak. The Agent Toolkit for AWS is built around the assumption that this sentence is wrong. Not because the sandbox is broken — the sandbox is fine — but because the agent's *job* is to do exactly what the developer would do, only faster. The sandbox is not a security boundary. The sandbox is an execution context. And in that execution context, the agent will read `~/.aws/credentials`, will call `aws secretsmanager get-secret-value`, will pipe the result into a `Bash` subshell, and will return the plaintext to a log line that gets committed to git. The sandbox does not stop this. The IAM policy does not stop this. The only thing that stops this is a hook that runs before the `Bash` call and says no. That hook is `plugins/aws-core/hooks/secret-safety.py`. The whole file is 94 lines. It runs in under five seconds. It is registered by `plugins/aws-core/hooks/hooks.json`, which attaches it as a `PreToolUse` matcher for `Bash` and for any `use_aws` or `mcp__aws.*` or `mcp__plugin_.*aws-mcp.*` tool call. When the agent wants to run a command, the agent host invokes the hook first, the hook reads the proposed tool call from stdin, and the hook returns a `permissionDecision: deny` JSON object on stdout if the call would fetch a secret. The agent never runs the call. The developer's credentials never reach the agent's context. The secret never gets exfiltrated. I assumed, walking into the repository, that the IAM condition keys were the headline security primitive. The README leads with them: "IAM condition keys that distinguish between agent actions and human actions, so you can write policies that apply only to agents. For example, you can write policies that only allow read-only actions through the MCP server, even if the user's underlying IAM role can take write actions." That is a real differentiator, and we will get to it. But the IAM primitive lives on the AWS side. It governs what the agent *can* do once the call reaches AWS. It does not govern what the agent *attempts* to do before the call leaves the developer's laptop. The hook is the boundary at the laptop edge. The IAM condition keys are the boundary at the AWS edge. The toolkit has both. You need both. ## The three primitives The Agent Toolkit for AWS's enterprise posture rests on three primitives. Each can be removed, and the toolkit degrades in a specific way. Each is small, auditable, and well-tested. The combination is what makes the toolkit shippable to a regulated environment. **Primitive 1: The PreToolUse hook.** A 30-line decision function that inspects every `Bash` and every AWS-tool call before it executes. The hook's decision logic is straightforward: 1. If the tool is `use_aws` or any `mcp__*` tool, extract the `service_name` (or its aliases) and the `operation_name` from the call's structured input. Normalize: lowercase, strip `-` and `_`. Compare to a small set of forbidden operations: `getsecretvalue`, `batchgetsecretvalue`. If the call would read a secret, deny. 2. As a fallback, search the entire JSON of the tool input for the regex `get[-_]?secret[-_]?value` and the substring `secretsmanager`. If both are present, deny. This catches malformed or nested calls that the structured check missed. 3. For `run_script` tools (the AWS MCP server's sandboxed Python execution environment), walk the string-valued fields and apply the same regex check. If a script would call `get_secret_value`, deny. 4. For `Bash` tools, regex-match the command string

Chapter 4 of 6 9m Article Audio Video Learning path

The PreToolUse Hook

The most important security primitive in the Agent Toolkit for AWS is a 30-line Python script that runs before every Bash call. Skip it, and the toolkit is AWS Labs with a new name.

Key Takeaways

  • The hook is a 30-line decision function registered as a PreToolUse matcher for Bash and any mcp__aws.* / use_aws call; it returns permissionDecision: deny if the call would fetch a secret.
  • The toolkit's enterprise posture rests on three primitives: the PreToolUse hook (laptop edge), the IAM condition keys (aws:CalledVia for AWS edge), and the secret resolution pattern ({{resolve:secretsmanager:...}} template + asm-exec).
  • Defense in depth: even if the hook fails to block, the secret resolution pattern prevents the value from reaching the agent's context anyway.
  • The hook is fail-open on timeout (timeout: 5) — a future hardening opportunity, but a real edge case to know.
  • The IAM condition keys are the AWS-side boundary; the hook is the laptop-side boundary. The toolkit ships both, and you need both for a regulated environment.

"But Claude Code is a sandbox."

I have heard this sentence in three different security review meetings in the last six months. The implication is that the coding agent runs in an isolated environment, that the agent's actions are not the developer's actions, and therefore that the agent cannot leak secrets because the agent has nothing to leak. The Agent Toolkit for AWS is built around the assumption that this sentence is wrong. Not because the sandbox is broken — the sandbox is fine — but because the agent's *job* is to do exactly what the developer would do, only faster. The sandbox is not a security boundary. The sandbox is an execution context. And in that execution context, the agent will read ~/.aws/credentials, will call aws secretsmanager get-secret-value, will pipe the result into a Bash subshell, and will return the plaintext to a log line that gets committed to git. The sandbox does not stop this. The IAM policy does not stop this. The only thing that stops this is a hook that runs before the Bash call and says no.

That hook is plugins/aws-core/hooks/secret-safety.py. The whole file is 94 lines. It runs in under five seconds. It is registered by plugins/aws-core/hooks/hooks.json, which attaches it as a PreToolUse matcher for Bash and for any use_aws or mcp__aws.* or mcp__plugin_.*aws-mcp.* tool call. When the agent wants to run a command, the agent host invokes the hook first, the hook reads the proposed tool call from stdin, and the hook returns a permissionDecision: deny JSON object on stdout if the call would fetch a secret. The agent never runs the call. The developer's credentials never reach the agent's context. The secret never gets exfiltrated.

I assumed, walking into the repository, that the IAM condition keys were the headline security primitive. The README leads with them: "IAM condition keys that distinguish between agent actions and human actions, so you can write policies that apply only to agents. For example, you can write policies that only allow read-only actions through the MCP server, even if the user's underlying IAM role can take write actions." That is a real differentiator, and we will get to it. But the IAM primitive lives on the AWS side. It governs what the agent *can* do once the call reaches AWS. It does not govern what the agent *attempts* to do before the call leaves the developer's laptop. The hook is the boundary at the laptop edge. The IAM condition keys are the boundary at the AWS edge. The toolkit has both. You need both.

The three primitives

The Agent Toolkit for AWS's enterprise posture rests on three primitives. Each can be removed, and the toolkit degrades in a specific way. Each is small, auditable, and well-tested. The combination is what makes the toolkit shippable to a regulated environment.

Primitive 1: The PreToolUse hook. A 30-line decision function that inspects every Bash and every AWS-tool call before it executes. The hook's decision logic is straightforward:

1. If the tool is use_aws or any mcp__* tool, extract the service_name (or its aliases) and the operation_name from the call's structured input. Normalize: lowercase, strip - and _. Compare to a small set of forbidden operations: getsecretvalue, batchgetsecretvalue. If the call would read a secret, deny. 2. As a fallback, search the entire JSON of the tool input for the regex get[-_]?secret[-_]?value and the substring secretsmanager. If both are present, deny. This catches malformed or nested calls that the structured check missed. 3. For run_script tools (the AWS MCP server's sandboxed Python execution environment), walk the string-valued fields and apply the same regex check. If a script would call get_secret_value, deny. 4. For Bash tools, regex-match the command string for aws secretsmanager (get-secret-value|batch-get-secret-value), for direct calls to the local Secrets Manager Agent at localhost:2773, and for any boto3/SDK call to get_secret_value. If any match, deny. 5. Otherwise, allow.

The hook is the *only* Python that runs at agent time. It is the *only* file in the toolkit that, if it fails, the agent's behavior changes. The plugins are JSON and Markdown. The MCP proxy is in a separate process tree. The hook is the one place where the agent host, the developer, and AWS share a single thread of execution.

Primitive 2: The IAM condition keys. The AWS MCP server attaches a aws:CalledVia context key to every call it makes, and a aws:CalledViaFirst key to the first call in a session. The developer's IAM policy can be written to check those keys: "Allow s3:GetObject only if aws:CalledVia is mcp-proxy-for-aws.amazonaws.com." The effect is that the agent can read objects only through the proxy, and the proxy's CloudTrail record is the only audit trail. If the agent (or a developer) tries to use the underlying IAM role to call S3 directly, the request is denied — the policy does not allow it. The two operations (agent-mediated, human-mediated) are gated by different condition keys, and the policy author can write them differently.

This is the primitive AWS Labs MCP servers could not provide. The condition keys exist for every AWS API call, but they are most useful when the caller is a managed service. The AWS MCP server qualifies. The earlier AWS Labs MCP servers, which were distributed as standalone Python packages that developers ran on their own machines, did not — their IAM calls were indistinguishable from a developer's direct call.

Primitive 3: The secret resolution pattern. The toolkit's aws-secrets-manager skill mandates that secrets never enter the agent's context. The skill instructs the agent to embed a template placeholder like {{resolve:secretsmanager:my-secret:SecretString:api-key}} into a command, and to invoke the asm-exec runtime resolver to swap the placeholder for the actual secret value at execution time. The agent never sees the resolved value. The asm-exec process runs in a sidecar container with read-only access to the secret and emits the value only into the executable's stdin or environment — never into a log line, never into an MCP response, never into the agent's memory.

This is the same pattern AWS CDK uses for its SecretValue API. The toolkit is not inventing a new primitive; it is reusing a well-known one and documenting the agent-side workflow. The result is that even if the PreToolUse hook fails to block a call, the secret resolution layer prevents the value from reaching the agent's context anyway. Defense in depth.

The constraints that make the hook work

The hook is small on purpose. The whole decision function is 94 lines of Python, with three regex patterns and a small set of operation-name normalizations. There is no allowlist of safe commands. There is no list of services to monitor. There is no dynamic plugin loader. The hook is not extensible; the comment at the top says "Use {{resolve:secretsmanager:secret-id:SecretString:key}} with asm-exec instead," and that is the entire API. The hook is a single-purpose tool.

flowchart TD
  A[Agent invokes a tool] --> B{Is it Bash or use_aws/mcp__*?}
  B -- No --> Z[Allow]
  B -- Yes --> C{Extract service + operation<br/>or scan command}
  C --> D{Service is secretsmanager<br/>and op is get/batch-get-secret-value?}
  D -- Yes --> E[DENY: use resolve template]
  C --> F{Regex get-secret-value<br/>AND secretsmanager in input?}
  F -- Yes --> E
  C --> G{run_script with get-secret-value<br/>in any string field?}
  G -- Yes --> E
  B2[Bash tool] --> H{aws secretsmanager get-secret-value<br/>in command?}
  H -- Yes --> E
  B2 --> I{localhost:2773/secretsmanager/get?}
  I -- Yes --> E
  B2 --> J{boto3 get_secret_value in command?}
  J -- Yes --> E
  D -- No --> Z
  F -- No --> Z
  G -- No --> Z
  H -- No --> Z
  I -- No --> Z
  J -- No --> Z

The diagram is the entire hook. Every edge is a regex match or a structured field check. Every deny is the same JSON response: a permissionDecision: deny with a reason string that points the agent at the correct replacement pattern.

The hook is registered with a timeout: 5 in hooks.json. Five seconds is generous — the hook runs in milliseconds — but the timeout is a guard against a hook that accidentally makes a network call. If the hook ever stalls, the agent host treats it as an allow (the default), and the agent can proceed. That default is the only weak spot in the design. A future version of the toolkit might consider treating a hook timeout as a deny, especially for security-critical hooks. The current behavior is "fail open."

What the IAM condition keys actually do

The aws:CalledVia context key is a list of services that handled the call, in order. The aws:CalledViaFirst context key is the first service. The AWS MCP server — when it calls S3 on behalf of the agent — appends mcp-proxy-for-aws.amazonaws.com to the list. The IAM policy can then read that key and decide.

The README's example is: "You can write policies that only allow read-only actions through the MCP server, even if the user's underlying IAM role can take write actions." Concretely, the policy looks like:

{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:ListBucket"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "aws:CalledVia": "mcp-proxy-for-aws.amazonaws.com"
    }
  }
}

A developer running aws s3 rm s3://my-bucket/file.txt from their terminal would have a different aws:CalledVia (empty, or the developer's CLI), and the policy would deny the call. The agent, mediated by the MCP proxy, would be allowed. The same IAM role, the same credentials, two different policies, two different outcomes. The agent's blast radius is smaller than the developer's, and the boundary is the aws:CalledVia key.

This is the headline security primitive of the toolkit, and it is real. But it is also a *trust* boundary, not a *correctness* boundary. If the agent's code path bypasses the proxy — for example, by shelling out to the local AWS CLI — the aws:CalledVia key will be empty, and the policy will deny the call. The proxy is the only way the agent reaches AWS in a way the policy recognizes. The hook is what prevents the agent from reaching AWS *without* the proxy.

The two primitives are complementary. The IAM condition keys are what the security team writes. The hook is what the developer gets in the box. The toolkit's value proposition is that you do not have to choose between them — you can ship both, and the agent's actions will be gated twice.

The boundary the toolkit does not draw

The hook does not enforce the IAM condition key; AWS does. The hook does not call asm-exec; the agent does. The hook does not authenticate the developer; the proxy does. The toolkit is a federation of small primitives, each of which does one thing well. The art of deploying it is deciding which primitive owns which boundary in your environment.

The hook is the only Python that runs at agent time. The skills, on the other hand, are 107 Markdown files. The next chapter opens that library and shows what the agent actually reads when a developer asks it to do something.