Skip to content

Pipe Lookup — Run Command And Return Output Locally


pipe is the most powerful lookup in this section, and the most dangerous one — it runs an arbitrary command on the control node, through a real shell, and hands back whatever it printed.

Running A Local Command With lookup('pipe', ...)

msg: "{{ lookup('pipe', 'date') }}"

Runs date — on the control node, following the same rule established over the last two chapters — captures its output, and returns it as a string with the trailing newline stripped.

A Realistic Example: Embedding A Local Git Commit Hash

A genuinely common practical use — tagging a deployment with the exact commit it was deployed from, read straight from the control node’s own local repository:

vars:
  git_commit: "{{ lookup('pipe', 'git rev-parse --short HEAD') }}"
tasks:
  - name: Tag deployment
    ansible.builtin.debug:
      msg: "Deploying commit {{ git_commit }}"

The Real Danger: Command Injection

pipe runs its command through an actual shell — meaning shell metacharacters in whatever string you build aren’t inert text, they’re live syntax, exactly the same danger eval-style command construction carries in any other context.

vars:
  user_supplied: "test; echo INJECTED"
tasks:
  - name: Vulnerable pipe usage
    ansible.builtin.debug:
      msg: "{{ lookup('pipe', 'echo ' + user_supplied) }}"
test
INJECTED

user_supplied was never meant to be anything but a piece of text — but concatenated directly into the command string, its semicolon became a genuine shell command separator. The shell ran two commands instead of one: echo test, then echo INJECTED. Whatever was embedded in that string got executed, not just printed.

The Fix: quote

This is exactly what quote, from the manipulating-data section’s strings chapter, exists for:

vars:
  user_supplied: "test; echo INJECTED"
tasks:
  - name: Safer pipe usage
    ansible.builtin.debug:
      msg: "{{ lookup('pipe', 'echo ' + (user_supplied | quote)) }}"
test; echo INJECTED

The entire string — semicolon and all — now prints as a single, literal, inert piece of text. quote wrapped it in shell-safe quoting before it ever reached the command string, so the shell sees one safely-escaped argument instead of a command followed by a second, separate command.

Best Practices

  • Never build a pipe command string by directly concatenating untrusted or variable data. Pass it through | quote first, without exception.
  • Treat lookup('pipe', ...) with the same caution as any other command-injection risk — it genuinely runs through a real shell, so unquoted variable data inside it is live syntax, not inert text.
  • Prefer pipe for mostly-static, control-node-side commands — a git commit hash, a local tool’s version — over commands dynamically assembled from variable or user-supplied input, whenever there’s a safer alternative available.
Last updated on