Getting a normal pass without normal maps (in Eevee)
I didn't find a solution so decided to write a script. It uses my scene setup so there's no point of pasting the whole thing but I'll paste a pseudo code that explains my approach
import bpy
import time

# disabling things
def get_color_management_settings():
  # returns some current values from scene.display_settings and view_settings: 
  # look, exposure, gamma, etc
  pass

def set_default_color_management_settings():
  # sets various display settings to a zeroed state 
  # so that the render wouldn't be affected by custom gamma/exposure/color profile settings
  pass

def set_color_management_settings(values):
  # sets various display settings to previously saved settigns
  pass

def disable_passes():
  # disables all the render passes but normal: breaks connections in compositing tab
  # renames 'normal' pass to 'geo_normal'
  # returns everything to restore to defaults:
  # file_output_node
  # render_output_node
  # outputs_arr: array of sockets that were connected
  pass  

def disable_materials():
  # for each material
  # disconnects Material Output from the shader node
  # creates a blank Diffuse node and connects Material Output with it
  # creates and returns a dict with mat_name: [original_input_node, original_input_socket]
  pass

def disable_lights():
  # gets and returns all the lights that aren't disabled in render
  # also disables them in render
  pass

def disable_volumes():
  # gets and returns all the objects with Volumetric materials
  # also disables them
  pass

# enabling things
def enable_passes(file_output_node, render_output_node, outputs_arr):
  # re-enables original nodes connections in the compositing tab
  # renames 'geo_normal' pass back to 'normal' pass
  pass

def enable_lights(lights):
  # re-enables all the lights that were disabled
  pass

def enable_materials(materials):
  # removes temporary Diffuse nodes
  # and reconnects original shader nodes with Material Output node
  pass

def enable_volumes(volumes):
  # re-enables objects with volumetric materials
  pass

def main():
  timeStart = time.perf_counter()

  # getting active color management settigs
  color_management_settings = get_color_management_settings()

  # setting default color management settings
  set_default_color_management_settings()

  # disable links in compositing
  file_output_node, render_output_node, outputs_arr = disable_passes()

  # disable objects with volumentric materials: saves around 30% of render time
  volumes = disable_volumes()

  # changes all materials to use a black diffuse node
  materials = disable_materials()

  # disable all the lights: saves render time
  lights = disable_lights()

  # render
  bpy.ops.render.render()

  # re-enable everything back
  enable_passes(file_output_node, render_output_node, outputs_arr)
  enable_volumes(volumes)
  enable_materials(materials)
  enable_lights(lights)
  set_color_management_settings(color_management_settings)
  timeEnd = time.perf_counter()
  
  # render takes about 1 second for simple scene, about 7-8 seconds for complex ones on 2070s
  print(f&quot;time: {timeEnd - timeStart:0.4f} seconds&quot;)

main()

The result: normals_geo.png with geo normals and correct gamma, normals.png that comes from default render setup: