Easy to use code preparation script for CodeReview questions
An alternative to defaulting variables by self-assigning with fallback is to use ${parameter:=word} at the point of first use:
for dir in ${IGNORED_DIRS:=build .git}; do


for ext in ${EXTENSIONS:=c cc cpp h hpp}; do

Actually, since those are the only uses, we could get away with :- expansion, even there.
Also, it makes sense to allow user to specify no ignored dirs and/or no extensions (and we check for that later in the code), so perhaps remove the : from those expansions.

RULE_GLUE can also be simplified:
RULE_GLUE=${PRUNE_RULES:+-o}

Again, it's not necessary to name this, but it might aid clarity.  Personally, I'd just add it directly to the end of the PRUNE_RULES:
PRUNE_RULES=&quot;${PRUNE_RULES} ${PRUNE_RULES:+-o}

or just ensure that each prune rule contains its own -o as it should:
for dir in ${IGNORED_DIRS}; do
  PRUNE_RULES=&quot;${PRUNE_RULES} -name ${dir} -prune -o&quot;  
done


The name rules can be simplified by starting with with the -false predicate (did you ever wonder what it's useful for?):
NAME_RULES=${EXTENSIONS:+-false}
for ext in ${EXTENSIONS}; do
    NAME_RULES=&quot;${NAME_RULES} -o -name '*.${ext}'&quot;
done


It's unspecified whether find expands {} when it's a substring of an argument; the safer POSIX way is to pass it as a positional argument and have the child shell expand it:
    -exec sh -c &quot;printf '${FILE_HEADING}' \&quot;\$1\" &amp;&amp; sed 's/^/    /' \$1&quot; -- {} ';'

It's probably a good idea to only match plain files (including symlinks to plain files), rather than everything that matches the name rules.  Especially if $NAME_RULES is empty, it's quite possible for a directory name to match.
I don't see why we need the explicit sh subshell as the final command (since we expand the &quot;rules&quot; predicates unquoted, so word splitting happens - though we'll need to disable globbing: set -f).  There's no good reason not to exec the final command.
Also, we need to be careful if $FILE_HEADING ever contains ': it might be safer to also pass this as a positional parameter, or to export the variable and have it expanded later:
export FILE_HEADING


exec find . ${PRUNE_RULES} ${NAME_RULES} -type f
    -exec sh -c 'printf &quot;${FILE_HEADING}&quot; &quot;$1&quot; &amp;&amp; sed &quot;s/^/    /&quot; &quot;$1&quot;' -- {} ';'

I think that rather than -exec sh, it might be simpler to have two -exec predicates:
... -exec printf &quot;${FILE_HEADING}&quot; {} \; -exec sed 's/^/    /' {} \;

(and we no longer need to export $FILE_HEADING).

Modified code
#!/bin/sh

# user-modifiable parameters
FILE_HEADING='## File: %s\n\n'
DEFAULT_IGNORED_DIRS='build .git'
DEFAULT_EXTENSIONS='c cc cpp h hpp'

# end of user variables; code starts here

set -eu  # Usual safety
set -f   # Don't expand pathnames when expanding $NAME_RULES below

for dir in ${IGNORED_DIRS-$DEFAULT_IGNORED_DIRS}; do
    PRUNE_RULES=&quot;${PRUNE_RULES-} -name ${dir} -prune -o&quot;  
done

for ext in ${EXTENSIONS-$DEFAULT_EXTENSIONS}; do
    NAME_RULES=&quot;${NAME_RULES:--false} -o -name *.${ext}&quot;
done

# We intend word splitting of rules variables
# shellcheck disable=SC2086
exec find -L . ${PRUNE_RULES-} ${NAME_RULES:+'(' $NAME_RULES ')'} -type f \
     -exec printf &quot;${FILE_HEADING}&quot; {} \; \
     -exec sed 's/^/    /' {} \;


Modified one-liner
find . -name \*.sh -exec printf '## File: %s\n\n' {} \; -exec sed 's/^/    /' {} \;