File Patterns and Hooks

The three filePatterns matching modes and minimatch rules, the after_write hook configuration and execution model, the --allow-hooks authorization mechanism, errorHandling strategies, and the security design behind them.

This page covers the two "fine control" capabilities of the generation pipeline: filePatterns, which decides which files get rendered, and hooks, which run scripts after files are written. Both are configured in .agilebuilder.config.yaml.

filePatterns: File Matching Rules

variables.filePatterns decides which files have their contents rendered with EJS:

variables:
  enabled: true
  filePatterns:
    mode: include
    patterns:
      - "**/*.ts"
      - "**/*.json"
      - "README.md"

The Three Modes

modeBehaviorTypical use case
allRender every non-binary file.Small templates with variables everywhere.
includeRender only files matching at least one pattern.Templates with many docs/examples that must not be rendered.
excludeRender files matching no pattern.Most files need rendering; only a few directories should be left alone.

Key points:

  • Patterns match against paths relative to the template root, always using / separators (backslashes are converted before matching on Windows).
  • Patterns use minimatch syntax with dot: true — paths with leading dots such as .github/workflows/ci.yml are matched by **.
  • A missing or empty patterns array is treated as ["**/*"]; an invalid mode falls back to all.
  • Files that do not match (not hit by include, or hit by exclude) are copied verbatim.
  • Matching only happens when variables.enabled: true; with false, every file is copied as-is.
  • Binary files are never rendered, regardless of the mode.

Common pattern examples:

PatternMatches
**/*.tsTypeScript files at any depth
package.jsonOnly the root-level package.json
src/**Everything under src/
**/*.{ts,tsx}TypeScript and TSX files

Path Rendering Is Not Affected by filePatterns

filePatterns controls content rendering only. As long as variables.enabled: true, {{var}} placeholders and EJS expressions in file paths are always rendered.

The after_write Hook

Hooks run after all files have been written to the target directory. Only the after_write stage is processed today, and only scriptType: shell can execute:

hooks:
  after_write:
    scriptType: shell
    script: npm install
    errorHandling: warn
    env:
      NPM_CONFIG_REGISTRY: https://registry.npmjs.org
FieldDefaultDescription
scriptTypeshellnodejs / custom are accepted in the config, but executing them once hooks are authorized fails with HOOK_TYPE_UNSUPPORTED — only shell runs today.
script— (required)Shell command executed in the target directory.
errorHandlingstopFailure strategy; see below.
envExtra environment variables, merged with the current process environment.

Execution model:

  • The working directory is the target directory from ag create --target, so commands like npm install or git init act directly on the generated project.
  • On Windows the CLI prefers bash -c; if bash is unavailable (for example, Git Bash is not installed), it falls back to the system default shell. Other platforms use the system shell directly.
  • The timeout is 5 minutes: on timeout the process receives SIGTERM, then SIGKILL after a 5-second grace period, and the CLI reports HOOK_TIMEOUT.
  • Hook stdout/stderr stream straight to your terminal for easy debugging.

Authorization: Why Hooks Do Not Run by Default

A template is third-party code fetched from a Git repository, and a hook is an arbitrary shell command executed with your user permissions. If hooks ran by default, anyone could run code on your machine at generation time simply by committing a malicious script into a template's config file. That is why AgileBuilder makes hook execution explicitly authorized:

  • Off by default: without authorization, after_write is recorded as skipped (hooksSkipped) and generation completes normally.
  • Per-run authorization: ag create ... --allow-hooks — always takes precedence.
  • Default authorization: ag config set template.allowHooksDefault true runs hooks even when --allow-hooks is not passed. Only enable this in environments where you trust every template source.

Skipping is never silent — the generation result lists skipped hooks in the hooksSkipped field, visible in both human-readable and --json output.

errorHandling: Failure Strategies

ValueBehavior
stop (default)Abort generation on hook failure with HOOK_FAILED; the process exits non-zero. Best when a failed hook means an unusable project (e.g. a mandatory dependency install).
warnRecord the failure as skipped (hooksSkipped) and finish generation normally. Best for nice-to-have scripts (formatting, an initial commit).
continueSame behavior as warn.

Other Security Measures

The generation pipeline has several more lines of defense, independent of hooks:

  • Target directory blacklist: writing to drive roots (such as C:\), the Unix root, C:\Windows, C:\Program Files, C:\Program Files (x86), C:\Program Files\Git, or anything beneath them is refused with UNSAFE_TARGET_DIR.
  • Non-empty directory protection: an existing non-empty target directory requires an explicit --overwrite, otherwise the command fails with TARGET_NOT_EMPTY.
  • Subdirectory escape protection: --subdir and source.subdir reject absolute paths and .. segments; the resolved path must stay inside the cloned template directory, or the command fails with TEMPLATE_SUBDIR_UNSAFE.
  • Config files stay out of the output: .agilebuilder.config.yaml / .agilebuilder.config.json are never copied to the target directory.
  • .git is skipped by default: unless --keep-git is passed, the output contains no Git history from the template repository.

Troubleshooting

  • Hook did not run? First confirm --allow-hooks was passed or template.allowHooksDefault is true, then check hooksSkipped in the output.
  • Variables not rendered? Check in order: is variables.enabled true → does the file match filePatterns → is the file binary?
  • Example files in the template that must not be rendered (EJS snippets in docs, say)? Exclude them with exclude mode, or switch to include to draw a tight boundary around what gets rendered.

What's Next