---
title: "cdkd diff: nested stacks"
description: "What cdkd diff --recursive shows for nested stacks, the nested templates it refuses to load, and how it handles nested-stack parameters that hold a secret."
---

# cdkd diff: nested stacks

`cdkd diff --recursive` looks inside nested stacks. Each nested stack is
compared with its own state, and its changes are printed in a block of their
own. Without `--recursive`, the diff stops at the parent, as `cdk diff` does.

```bash
# the parent only
cdkd diff ParentStack

# the parent and every nested stack below it
cdkd diff ParentStack --recursive
```

`--fail-on=destructive` always looks inside nested stacks, with or without
`--recursive`.

## What `--recursive` shows

Without the flag, a changed nested stack appears as one line in the parent:
its `AWS::CloudFormation::Stack` resource, with a `TemplateURL` or
`Parameters` change. Nothing inside the nested stack is inspected.

With the flag, each nested stack that has changes gets a block under a
`Nested stack: <name>` header. The name is the nested stack's full name in
state. It joins the levels with `~`, such as `ParentStack~Child`, and is the
name `cdkd state show --show-nested` prints. Nested stacks with no changes are
checked and print nothing.

Three situations produce a block where every resource has the same marker:

| The nested stack | Every resource shows as |
| --- | --- |
| Has no state file yet | A create |
| Was removed from the CDK code | A delete |
| Has a `Condition` that is now false | A delete |

A removed nested stack is followed all the way down, so the nested stacks
inside it show as deletes too. The third row applies only when the diff can
evaluate the condition; see
[Conditions over a secret-fed parameter](#conditions-over-a-secret-fed-parameter).

A nested stack whose state record is damaged is reported with a warning; see
[cdkd diff: incomplete or malformed state](cli-diff-state-records.md#when-the-state-record-is-malformed).

### Parameters passed to a nested stack

The parent passes values to a nested stack through its `Parameters`. The diff
resolves them the way `cdkd deploy` does before it compares. A `Ref` to one of
the parent's parameters, or an `Fn::If`, is therefore compared as the value
the deploy would send.

### A change the preview cannot list

One change is invisible to the diff. A resource in a nested stack can read the
value of a `NoEcho` custom resource through a stack parameter. The diff sees
`***` on both sides, so it reports nothing. The deploy updates that resource
when the parent's custom resource returns a new value.

## Cyclic nested templates are refused

The diff refuses a set of templates in which a nested stack contains itself,
directly or through other nested stacks. It exits `1` with this error:

```text
Nested stack Child under stack Parent~Child resolves to nested template
/path/to/cdk.out/child.json, which is already being diffed higher up the
same nesting chain. Its Metadata['aws:asset:path'] closes a cycle; CDK emits
an acyclic nested template tree, so this indicates the synth output was
hand-modified or generated by a non-CDK toolchain. Refusing to diff.
```

CDK never produces a cycle, so a `cdk.out` directory that CDK wrote does not
hit this. You can reach it only with synthesis output that was edited by hand
or produced by another tool. The error names the nested stack resource and the
template file that closed the cycle.

The diff finds each nested template through the `aws:asset:path` entry in the
nested stack resource's `Metadata`. These cases define what counts as a cycle:

| Case | Result |
| --- | --- |
| Two sibling nested stacks use the same template | Allowed |
| The same file reached through a symbolic link | Counts as the same template |
| A cycle through a nested stack whose `Condition` is false | Refused |
| More than 512 levels of nesting | Refused |
| More than 10,000 nested stacks | Refused |

A cycle through a false `Condition` is refused with the wording `cdkd deploy`
uses: `The nested template tree under stack ... contains a cycle`.

## Nested templates outside the assembly directory are refused

The diff also refuses a nested template whose path points outside the
directory its parent template is in:

```text
Nested stack Child has Metadata['aws:asset:path']=../../etc/passwd which
resolves to /etc/passwd, outside /path/to/cdk.out. CDK emits assembly paths
that stay inside the assembly directory; one that leaves it indicates the synth
output was hand-modified or generated by a non-CDK toolchain. Refusing to load.
```

Two related paths are refused as well:

- A path that leaves the directory only after a symbolic link is followed.
- An absolute path. A separate check refuses it, and its message says
  `which is absolute`.

`cdkd deploy` applies the same rules; see
[Cyclic nested templates are refused](cli-deploy-safety-assembly-checks.md#cyclic-nested-templates-are-refused).

## Nested stacks and secret references

This section applies when a parent passes a secret to a nested stack as a
parameter. A secret here means a dynamic reference:
`{{resolve:secretsmanager:...}}`, `{{resolve:ssm-secure:...}}`, or
`{{resolve:ssm:...}}` naming a `SecureString`.

The diff does not decrypt the parameter. It passes the `{{resolve:...}}` text
down to the nested stack as the parameter's value. The nested stack's state
holds the same text, so the diff compares one reference with another and no
plaintext is printed.

This has one consequence. The diff assumes that the secret behind an unchanged
reference has not changed.

### A phantom change on a list-typed parameter

A list-typed parameter is split on commas. A reference that contains a comma
in its JSON-key part, such as
`{{resolve:secretsmanager:sec:SecretString:a,b::}}`, is split in the wrong
place. The diff then reports a change on every `cdkd diff --recursive`,
although nothing changed. Nothing is written and no plaintext is exposed.

How each parameter `Type` is compared is in
[cdkd diff internals](cli-diff-internals.md#how-a-secret-fed-nested-parameter-is-compared).

### Conditions over a secret-fed parameter

A template condition can depend on a parameter that holds a secret, directly
or through another condition. The diff cannot evaluate such a condition,
because it does not know the secret's value. The same applies to a condition
over a template parameter the diff cannot bind. Every other condition is
evaluated as usual.

For a condition it cannot evaluate, the diff reuses the answer the last
`cdkd deploy` recorded in the nested stack's state. It does so only while the
condition, the conditions it names, and the inputs of its parameters are
unchanged since that deploy.

When there is no recorded answer it can reuse, the diff falls back to two
rules:

- An `Fn::If` on the condition takes its false branch.
- A resource gated on the condition is kept and compared. It is not removed.

The fallback can be wrong in both directions. It can show a change the deploy
will not make. It can also hide one: after a deploy where the condition was
false, an edit that makes it true still shows as no change. When a recorded
answer exists but no longer matches, the diff warns and names the condition.

There is no recorded answer the diff can reuse in these situations:

- The condition or its inputs changed since the last deploy.
- The state was written by an older cdkd, or by a deploy that failed.
- The condition reads an SSM-typed parameter or a `NoEcho` parameter.

The complete list is in
[cdkd diff internals](cli-diff-internals.md#when-a-recorded-condition-verdict-is-reused).

### Secret parameters must be `Type: String`

A nested-stack parameter that receives a secret reference must be declared
`Type: String`. CDK declares every such parameter as `Type: String`, so an
app written with CDK does not hit this rule.

`cdkd deploy` refuses a `Type: Number` or `Type: List<Number>` parameter in
that position, and names the parameter:

```text
Nested-stack parameter 'DbPort' is declared 'Type: Number', but the parent
stack resolved a SECRET dynamic reference into it. ...
```

The reason is how cdkd keeps secrets out of state. It recognises a secret
value as a string and writes the reference back in its place. A value
converted to a number can no longer be recognised, so the decrypted secret
would stay in the nested stack's state. `cdkd diff` warns for any parameter
whose declared type could cause this.

`CommaDelimitedList` and the other `List<...>` types are allowed as long as
the secret value contains no comma. A JSON secret always contains commas, so a
list-typed parameter works only for a secret that is a single plain value.

## Related

- [`cdkd diff`](cli-diff.md): the text output, the exit codes and the options
- [cdkd diff: secrets and NoEcho values](cli-diff-secrets.md): what the diff prints in place of a secret
- [cdkd deploy: safety & compatibility flags](cli-deploy-safety.md): the same nested-template refusals on deploy
- [cdkd diff internals](cli-diff-internals.md): how parameters and conditions are compared
