| name | pike-safe-policy |
| description | pike++ — AI least-privilege layer on top of pike's deterministic permission scan. Adds Resource ARN scoping and Conditions to escalation-class actions, splits base/escalation into the two-role pattern, and validates the result against pike's own audit rules until clean. |
pike++
pike scan is the deterministic engine: given Terraform, it produces the exact, correct set of IAM actions required — no more, no less. That part is a solved problem and this skill must never second-guess it. What pike cannot do is decide who those actions should apply to and under what conditions — that requires reading the actual resource names in the config and reasoning about intent, which is exactly what a rules engine structurally can't do. pike++ is you: the layer that turns pike's action list into an actual least-privilege policy.
Arguments:
- Optional: a directory to scan (defaults to
.)
- Usage:
/pike-safe-policy or /pike-safe-policy ./terraform
The gap, precisely
Don't take this on faith — it's visible in pike's own source:
src/types.go — Statement has Sid, Effect, Action, Resource only. No Condition field exists in the type. pike cannot emit a conditioned statement even in principle, today.
src/policy.go:58-60 — myResource := []string{"*"} / resource := "*" is hardcoded before the ARN builder ever runs. Every generated statement's Resource is "*", regardless of what the Terraform actually names.
src/escalation.go (escalationAWS/escalationGCP/escalationAZURE) and WarnEscalation already identify which actions are escalation-class and recommend a two-role split — but pike only warns; it doesn't produce the two policy documents.
src/audit_aws.go / audit_gcp.go / audit_azure.go already know what a safe Condition looks like (see escalationScopingConditionKeys in audit_aws.go) — that judgment exists in the codebase for reviewing hand-written policies, but nothing wires it into generation.
So: pike tells you the floor (never remove an action pike says is required). This skill's only job is to add restriction on top — Resource scoping and Conditions — never to narrow the action list itself. If you find yourself dropping an action pike listed because "it looks too broad," stop — that's a bug in this skill, not a fix.
What you produce
Two IAM policy documents per provider, mirroring pike's own two-role recommendation:
- Planner —
PlanAWS/PlanGCP/PlanAZURE actions only (read-only, safe for terraform plan on every branch). Never contains an escalation-class action.
- Applier — the full
Sorted.AWS/GCP/AZURE action list (everything, including escalation-class), scoped down with Resource ARNs and Conditions. Restricted to protected branches / a gated role.
Every Resource and Condition you add must be traceable to something literal in the Terraform, or to a real datasource reference Terraform will resolve at apply time (see step 2). Where a name is genuinely unresolvable either way (computed at apply time via random_id/random_pet, or supplied only through a variable with no default), leave that specific ARN segment as * and say so explicitly in the rationale — a guessed ARN that's wrong is worse than an honest wildcard, because it fails closed silently instead of visibly.
Process
-
Get the deterministic floor. Run:
! pike scan -o split -d <dir> — base/escalation action split per provider (SplitPolicy JSON: {"aws":{"base":[...],"escalation":[...]}, ...})
! pike scan -o json -d <dir> — the full Sorted bag, including PlanAWS/PlanGCP/PlanAZURE (planner-role actions) and RuntimeAWS/RuntimeGCP/RuntimeAZURE (per-resource runtime permissions, already resource-attributed)
These three lists are ground truth. Every action in your two output policies must come from here — nothing added, nothing removed.
-
Read the Terraform. For every resource that contributed actions to the bag above, find its declaration and pull out anything ARN-able: literal bucket/table/role/key/queue names, name_prefix, explicit arn references to sibling resources declared in the same config, account/region if hardcoded via provider blocks. Build a resource → ARN-pattern map. Where a resource name is computed (e.g. "${var.env}-${random_id.suffix.hex}"), use the literal parts and wildcard only the unresolvable segment — don't wildcard the whole ARN just because part of it is dynamic.
Prefer a datasource reference over a wildcard for account/project/subscription segments. The output of this skill is Terraform, not static JSON — so an account ID, project ID, or subscription ID that can't be known statically doesn't have to become *. It can be a real interpolation that Terraform resolves correctly at apply time: data.aws_caller_identity.current.account_id (AWS), data.google_project.current.project_id (GCP), data.azurerm_client_config.current.subscription_id (Azure). Check whether the scanned Terraform already declares the equivalent datasource block and reuse that reference rather than declaring a duplicate; otherwise add the datasource block to the generated policy's own .tf output and note that it's new. Only fall back to a literal * for a segment that no well-known "current context" datasource can resolve either (e.g. a cross-account target ARN supplied only via a variable with no default, or a name that's genuinely random at apply time via random_id/random_pet).
-
Scope each statement's Resource. Group actions by the resource(s) they were attributed to (pike's / already gives you this attribution for runtime permissions; for apply/plan actions, re-derive it from which Terraform block required which action during step 2). Emit one statement per resource-group with set to the ARN pattern(s) from step 2, not . Actions that are inherently account/region-scoped only (no per-resource ARN in that service, e.g. ) legitimately keep — say so, don't leave it unexplained.
Discipline
- Never remove or substitute an action pike listed. If an action looks wrong, that's a pike bug to report separately — this skill only adds restriction, never edits the action set.
- Never fabricate an ARN or account ID. Prefer a datasource reference (
data.aws_caller_identity.current.account_id and equivalents — see step 2) over a literal guess. Only fall back to wildcarding the segment when no datasource can resolve it either, and say why.
- Never add a Condition that doesn't actually restrict anything just to make
pike audit stop complaining — that defeats the entire point and is worse than the unconditioned finding, because it looks fixed on review.
- The validation loop (step 5) is mandatory, not optional. A hand-scoped policy that hasn't been re-run through
pike audit is a draft, not an output. But know what it did and didn't check: it verifies Conditions on escalation actions reliably; it does not verify a datasource-interpolated Resource is well-formed (see step 5's blind-spot note) — don't report "audit passed" as if it covered both.
- When genuinely stuck (no derivable scoping, an iteration cap hit, a resource type pike/audit doesn't know), say so plainly in the report rather than emitting a plausible-looking but unverified policy.
Status
Validated on a single hand-crafted AWS fixture (S3 bucket + IAM role assumed by a Lambda + the Lambda function) — the deterministic-floor extraction, ARN scoping, Condition derivation, and the pike audit validation loop all worked, including correctly refusing to fabricate a Condition for iam:PutRolePolicy when none was derivable and reporting that honestly instead. A follow-up test confirmed the checker's blind spot documented in step 5: swapping a literal Resource for a ${data.aws_caller_identity.current.account_id} interpolation makes AWS003 (wildcard check) go silent either way, while AWS004 (Condition check) still fires correctly — so a clean audit run only certifies Conditions, not interpolated Resources.
Not yet exercised: GCP/Azure targets, computed/interpolated resource names beyond the one datasource case above, multi-resource-of-same-type attribution, and the step 7 "propose a new audit rule" path. Treat output as a strong draft to review, not a policy to attach unreviewed.