Remote templates: symlinks are silently dropped since 1.4.1, breaking shared code across template variants
@mhenc is already working on this.
Since Sep 14, 2026.
Assessment
This issue has not been assessed yet.
Description
What happened
Since 1.4.1, agents-cli scaffold create --agent <remote git URL> silently drops every
symlink in the template source. A template that uses an intra-repo symlink to share a
library across several template variants scaffolds "successfully" into a project that is
missing that library and fails on import.
This is a regression: 1.4.0 copied a symlinked directory as real files.
The guard is copy_files.should_skip() in
google/agents/cli/scaffold/utils/template.py:
# Never follow symlinks from untrusted remote template sources
if path.is_symlink():
logging.warning(
f"Skipping symlink in template source (symlinks are not allowed): {path}"
)
return True
I understand the CWE-59 motivation (a template shipping id_rsa -> ~/.ssh/id_rsa). The
problem is that the check rejects all symlinks, including ones whose target never leaves
the cloned repository, and it does so without failing the command.
Reproduction
A repo laid out like this, pushed to GitHub, with the template at template/:
myrepo/
├── shared/ # single source of truth
│ └── config.py
└── template/
├── agents-cli-manifest.yaml
└── app/
├── agent.py # `from .shared.config import ...`
└── shared -> ../../shared # committed as a symlink (git mode 120000)
agents-cli scaffold create demo \
--agent https://github.com/<org>/myrepo/template@main --prototype
Expected: demo/app/shared/config.py exists (this is what 1.4.0 did).
Actual: demo/app/shared/ does not exist. The command prints
✅ Success! Your agent project is ready. and the project fails at import.
Version bisect
I ran each release's own copy_files over the same template directory:
| Version | app/shared/ in output |
|---|---|
| 1.0.0 / 1.2.0 / 1.3.0 / 1.4.0 | present (symlink dereferenced) |
| 1.4.1 / 1.5.0 | missing |
The Never follow symlinks from untrusted remote string first appears in 1.4.1.
Two things make this hard to notice
- It is a warning, not an error. Scaffolding reports success, and the failure only
shows up later as anImportErrorfar from its cause. local@does not exercise the same path.agents-cli scaffold create --agent local@<path>pre-copies the template with
shutil.copytree(local_path, template_source_path, ignore=...)
(scaffold/commands/create.py), andcopytreedefaults tosymlinks=False, i.e. it
dereferences. So a template author's CI that validates withlocal@— which the docs
present as the way to test a template locally — passes, while every real user of the
published URL gets a broken project. Our CI has been green this whole time.
Why this matters for multi-variant templates
We maintain an internal ADK template with several variants (a default ReAct agent, an A2UI
variant, a multi-agent GKE variant). All of them share one library — DI wiring for model
tiers, Secret Manager, artifact storage, logging/tracing — plus one ruff.toml and one
ty.toml.
The scaffolder only copies the directory --agent points at, so the only way to share
code across variants without duplicating it is an intra-repo symlink from each variant
into the shared directory. That is exactly what 1.4.1 broke. This is the same use case as
#62, which we're otherwise happy with.
The workaround is to vendor N physical copies of the shared library into the repo (one per
variant) and add a sync script plus a CI drift check. That works, but it means the "single
source of truth" only exists by convention, and every shared-code review carries N
mechanical duplicate diffs.
Suggested fixes, in order of preference
-
Allow a symlink whose resolved target stays inside the fetched repository.
Path.resolve()the link and require it to be under the clone root, skipping it
otherwise. That preserves the CWE-59 protection completely — an escaping link is still
refused — while letting a template share code within its own repo.One detail: the containment check needs to be against the cloned repo root, not the
template subdirectory. A shared library naturally sits at the repo root while the
template is a subdirectory, so the link legitimately points "up" out of the template
dir but never out of the repo. -
An opt-in in the template manifest, e.g.
settings.follow_symlinks: true, if you'd
rather the decision be explicit per template. -
At minimum, fail loudly. If symlinks stay banned, please make the command error out
(or print to stderr and list every dropped path in the summary) instead of logging a
warning and reporting success. Silently producing a broken project is the worst
outcome. It would also help to havelocal@skip symlinks the same way remote fetches
do, so a template author's local validation reflects what users will actually get.
Environment
- agents-cli 1.5.0 (and 1.4.1); last working: 1.4.0
- Linux, Python 3.13 / 3.14
- Dominant language
- Python
- Stars
- 6k
- Forks
- 670
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from google/agents-cli
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
google/agents-cli#86 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
google/agents-cli#85 · 1 comment ·
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
google/agents-cli#84 · 1 comment ·
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
google/agents-cli#83 · 2 comments · 1 reaction ·
-
google/agents-cli#79 · 2 comments · 1 assignee ·
All issues in google/agents-cli
Similar issues
-
essnmx good first issue
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
[Feature] 奇物选择添加优先级 Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
syfoud/Simulated_Scepter#174 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Giskard-AI/giskard-oss#2840 · 1 comment ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Openarea: repo bug perceived difficulty: 2
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
yeti-platform/yeti#1380 ·